Repository: redhat-developer/service-binding-operator
Pull request #566 (2020-07-17 09:28:07)

Ran operator-sdk generate crds on master
Run operator-sdk generate crds on master, resulted in alphabetically arranged endpoints.

openshift-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **avni-sharma**
You can assign the PR to them by writing `/assign @avni-sharma` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-developer%2Fservice-binding-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/redhat-developer/service-binding-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["avni-sharma"]} -->

Patch:
diff --git a/.gitignore b/.gitignore
index 68b88de5..7bdf68ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,5 @@ tags
 .idea
 ### Logs
 **/*.log
+venv
+**/*/__pycache__
diff --git a/Makefile b/Makefile
index 08b96e9f..84be7a76 100644
--- a/Makefile
+++ b/Makefile
@@ -156,11 +156,14 @@ BASE_COMMIT := $(shell echo $$CLONEREFS_OPTIONS | jq '.refs[0].base_sha')
 PR_COMMIT := $(shell echo $$CLONEREFS_OPTIONS | jq '.refs[0].pulls[0].sha')
 PULL_NUMBER := $(shell echo $$CLONEREFS_OPTIONS | jq '.refs[0].pulls[0].number')
 
+# -- Variables for acceptance tests
+TEST_ACCEPTANCE_START_SBO ?= local
+
 ## -- Static code analysis (lint) targets --
 
 .PHONY: lint
 ## Runs linters on Go code files and YAML files - DISABLED TEMPORARILY
-lint: setup-venv lint-go-code lint-yaml
+lint: setup-venv lint-go-code lint-yaml lint-python-code
 
 YAML_FILES := $(shell find . -path ./vendor -prune -o -type f -regex ".*y[a]ml" -print)
 .PHONY: lint-yaml
@@ -179,6 +182,11 @@ lint-go-code: $(GOLANGCI_LINT_BIN)
 $(GOLANGCI_LINT_BIN):
 	$(Q)curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./out v1.18.0
 
+## -- Check the python code
+.PHONY: lint-python-code
+lint-python-code:
+	$(Q)./hack/check-python/lint-python-code.sh
+
 .PHONY: setup-venv
 ## Setup virtual environment
 setup-venv:
@@ -267,6 +275,31 @@ test-unit-with-coverage:
 		go test $(shell GOCACHE="$(GOCACHE)" go list ./...|grep -v e2e) $(GOCOV_FLAGS) -v -mod vendor $(TEST_EXTRA_ARGS)
 	$(Q)GOCACHE=$(GOCACHE) go tool cover -func=$(GOCOV_FILE)
 
+.PHONY: test-acceptance-setup
+## Setup the environment for the acceptance tests
+ifeq ($(TEST_ACCEPTANCE_START_SBO), local)
+test-acceptance-setup:
+	$(Q)echo "Starting local SBO instance"
+	$(eval TEST_ACCEPTANCE_SBO_STARTED := $(shell OPERATOR_NAMESPACE="$(TEST_NAMESPACE)" ZAP_FLAGS="$(ZAP_FLAGS)" ./hack/deploy-sbo-local.sh))
+else ifeq ($(TEST_ACCEPTANCE_START_SBO), operator-hub)
+test-acceptance-setup:
+	$(eval TEST_ACCEPTANCE_SBO_STARTED := $(shell ./hack/deploy-sbo-operator-hub.sh))
+endif
+
+.PHONY: set-test-namespace
+set-test-namespace: get-test-namespace
+	$(Q)oc project $(TEST_NAMESPACE)
+
+.PHONY: test-acceptance
+## Runs acceptance tests
+test-acceptance: e2e-setup set-test-namespace deploy-clean deploy-rbac deploy-crds test-acceptance-setup
+	$(Q)echo "Running acceptance tests"
+	$(Q)TEST_ACCEPTANCE_START_SBO=$(TEST_ACCEPTANCE_START_SBO) \
+		TEST_ACCEPTANCE_SBO_STARTED=$(TEST_ACCEPTANCE_SBO_STARTED) \
+		TEST_NAMESPACE=$(TEST_NAMESPACE) \
+		behave -v --no-capture --no-capture-stderr --tags="~@disabled" test/acceptance/features
+	$(Q)kill $(TEST_ACCEPTANCE_SBO_STARTED)
+
 .PHONY: test
 ## Test: Runs unit and integration (e2e) tests
 test: test-unit test-e2e
@@ -481,3 +514,4 @@ dev-release:
 validate-release: setup-venv
 	$(Q)$(OUTPUT_DIR)/venv3/bin/pip install yq==2.10.0
 	BUNDLE_VERSION=$(BASE_BUNDLE_VERSION) CHANNEL="alpha" ./hack/validate-release.sh
+
diff --git a/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml b/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml
index 818d87ae..6c0dbb8c 100644
--- a/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml
+++ b/deploy/crds/apps.openshift.io_servicebindingrequests_crd.yaml
@@ -104,8 +104,12 @@ spec:
                 service operator. Deprecation Notice: In the upcoming release, this
                 field would be depcreated. It would be mandatory to set "backingServiceSelectors".'
               properties:
+                envVarPrefix:
+                  type: string
                 group:
                   type: string
+                id:
+                  type: string
                 kind:
                   type: string
                 namespace:
@@ -114,10 +118,6 @@ spec:
                   type: string
                 version:
                   type: string
-                envVarPrefix:
-                  type: string
-                id:
-                  type: string
               required:
               - group
               - kind
@@ -132,8 +132,12 @@ spec:
                 description: BackingServiceSelector defines the selector based on
                   resource name, version, and resource kind
                 properties:
+                  envVarPrefix:
+                    type: string
                   group:
                     type: string
+                  id:
+                    type: string
                   kind:
                     type: string
                   namespace:
@@ -142,10 +146,6 @@ spec:
                     type: string
                   version:
                     type: string
-                  envVarPrefix:
-                    type: string
-                  id:
-                    type: string
                 required:
                 - group
                 - kind
diff --git a/deploy/role.yaml b/deploy/role.yaml
index 8296c728..86d4d5e0 100644
--- a/deploy/role.yaml
+++ b/deploy/role.yaml
@@ -1,7 +1,6 @@
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
 metadata:
-  creationTimestamp: null
   name: service-binding-operator
 rules:
   - apiGroups:
@@ -14,6 +13,7 @@ rules:
       - events
       - configmaps
       - secrets
+      - pods/log
     verbs:
       - "*"
   - apiGroups:
@@ -23,8 +23,19 @@ rules:
       - daemonsets
       - replicasets
       - statefulsets
+      - deployments/finalizers
     verbs:
       - "*"
+  - apiGroups:
+      - apiextensions.k8s.io
+    resources:
+      - customresourcedefinitions
+      - customresourcedefinitions/status
+    verbs:
+      - get
+      - list
+      - patch
+      - watch
   - apiGroups:
       - monitoring.coreos.com
     resources:
@@ -61,3 +72,10 @@ rules:
       - serving.knative.dev
     resources:
       - services
+  - apiGroups:
+      - route.openshift.io
+    resources:
+      - routes
+    verbs:
+      - get
+      - list
diff --git a/hack/check-python/check-PEP8-style.sh b/hack/check-python/check-PEP8-style.sh
new file mode 100755
index 00000000..359ceb9d
--- /dev/null
+++ b/hack/check-python/check-PEP8-style.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+. ./hack/check-python/prepare-env.sh
+
+echo "----------------------------------------------------"
+echo "Running Python linter against following directories:"
+echo "$directories"
+echo "----------------------------------------------------"
+echo
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+# checks for the whole directories
+for directory in $directories
+do
+    files=$(find "$directory" -path "$directory/venv" -prune -o -name '*.py' -print)
+
+    for source in $files
+    do
+        echo "$source"
+        pycodestyle "$source"
+        if [ $? -eq 0 ]
+        then
+            echo "    Pass"
+            let "pass++"
+        else
+            echo "    Fail"
+            let "fail++"
+        fi
+    done
+done
+
+
+if [ $fail -eq 0 ]
+then
+    echo "All checks passed for $pass source files"
+else
+    let total=$pass+$fail
+    echo "Linter fail, $fail source files out of $total source files need to be fixed"
+    exit 1
+fi
diff --git a/hack/check-python/check-python-docstyle.sh b/hack/check-python/check-python-docstyle.sh
new file mode 100755
index 00000000..28bb2e64
--- /dev/null
+++ b/hack/check-python/check-python-docstyle.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+
+. ./hack/check-python/prepare-env.sh
+
+# run the pydocstyle for all files that are provided in $1
+function check_files() {
+    for source in $1
+    do
+        echo "$source"
+        pydocstyle --count "$source"
+        if [ $? -eq 0 ]
+        then
+            echo "    Pass"
+            let "pass++"
+        elif [ $? -eq 2 ]
+        then
+            echo "    Illegal usage (should not happen)"
+            exit 2
+        else
+            echo "    Fail"
+            let "fail++"
+        fi
+    done
+}
+
+
+echo "----------------------------------------------------"
+echo "Checking documentation strings in all sources stored"
+echo "in following directories:"
+echo "$directories"
+echo "----------------------------------------------------"
+echo
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+# checks for the whole directories
+for directory in $directories
+do
+    files=$(find "$directory" -path "$directory/venv" -prune -o -name '*.py' -print)
+
+    check_files "$files"
+done
+
+
+if [ $fail -eq 0 ]
+then
+    echo "All checks passed for $pass source files"
+else
+    let total=$pass+$fail
+    echo "Documentation strings should be added and/or fixed in $fail source files out of $total files"
+    exit 1
+fi
diff --git a/hack/check-python/detect-common-errors.sh b/hack/check-python/detect-common-errors.sh
new file mode 100755
index 00000000..13b0499e
--- /dev/null
+++ b/hack/check-python/detect-common-errors.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+. ./hack/check-python/prepare-env.sh
+
+# run the pyflakes for all files that are provided in $1
+function check_files() {
+    for source in $1
+    do
+        echo "$source"
+        pyflakes "$source"
+        if [ $? -eq 0 ]
+        then
+            echo "    Pass"
+            let "pass++"
+        elif [ $? -eq 2 ]
+        then
+            echo "    Illegal usage (should not happen)"
+            exit 2
+        else
+            echo "    Fail"
+            let "fail++"
+        fi
+    done
+}
+
+
+echo "----------------------------------------------------"
+echo "Checking source files for common errors in following"
+echo "directories:"
+echo "$directories"
+echo "----------------------------------------------------"
+echo
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+# checks for the whole directories
+for directory in $directories
+do
+    files=$(find "$directory" -path "$directory/venv" -prune -o -name '*.py' -print)
+
+    check_files "$files"
+done
+
+
+if [ $fail -eq 0 ]
+then
+    echo "All checks passed for $pass source files"
+else
+    let total=$pass+$fail
+    echo "$fail source files out of $total files needs to be checked and fixed"
+    exit 1
+fi
diff --git a/hack/check-python/detect-dead-code.sh b/hack/check-python/detect-dead-code.sh
new file mode 100755
index 00000000..2c8c0e14
--- /dev/null
+++ b/hack/check-python/detect-dead-code.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+. ./hack/check-python/prepare-env.sh
+
+# run the vulture for all files that are provided in $1
+function check_files() {
+    for source in $1
+    do
+        echo "$source"
+        vulture --min-confidence 90 "$source"
+        if [ $? -eq 0 ]
+        then
+            echo "    Pass"
+            let "pass++"
+        elif [ $? -eq 2 ]
+        then
+            echo "    Illegal usage (should not happen)"
+            exit 2
+        else
+            echo "    Fail"
+            let "fail++"
+        fi
+    done
+}
+
+
+echo "----------------------------------------------------"
+echo "Checking source files for dead code and unused imports"
+echo "in following directories:"
+echo "$directories"
+echo "----------------------------------------------------"
+echo
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+# checks for the whole directories
+for directory in $directories
+do
+    files=$(find "$directory" -path "$directory/venv" -prune -o -name '*.py' -print)
+
+    check_files "$files"
+done
+
+
+if [ $fail -eq 0 ]
+then
+    echo "All checks passed for $pass source files"
+else
+    let total=$pass+$fail
+    echo "$fail source files out of $total files seems to contain dead code and/or unused imports"
+    exit 1
+fi
diff --git a/hack/check-python/lint-python-code.sh b/hack/check-python/lint-python-code.sh
new file mode 100755
index 00000000..7fde34fd
--- /dev/null
+++ b/hack/check-python/lint-python-code.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+failed=""
+CHECK_PYTHON=./hack/check-python
+$CHECK_PYTHON/detect-common-errors.sh || failed="$failed\n - detect-common-errors"
+$CHECK_PYTHON/detect-dead-code.sh || failed="$failed\n - detect-dead-code"
+$CHECK_PYTHON/check-PEP8-style.sh || failed="$failed\n - check-PEP8-style"
+#$CHECK_PYTHON/check-python-docstyle.sh || failed="$failed\n - check-python-docstyle"
+$CHECK_PYTHON/measure-cyclomatic-complexity.sh || failed="$failed\n - measure-cyclomatic-complexity"
+$CHECK_PYTHON/measure-maintainability-index.sh || failed="$failed\n - measure-maintainability-index"
+
+if [ ! -z "$failed" ]; then
+    echo -e "\nERROR: Following python checks FAILED:$failed\n"
+    exit 1
+else
+    echo -e "\nAll python checks PASSED"
+fi
diff --git a/hack/check-python/measure-cyclomatic-complexity.sh b/hack/check-python/measure-cyclomatic-complexity.sh
new file mode 100755
index 00000000..4523bf5b
--- /dev/null
+++ b/hack/check-python/measure-cyclomatic-complexity.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+. ./hack/check-python/prepare-env.sh
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+
+pushd "${SCRIPT_DIR}/.."
+radon cc -s -a -i venv .
+popd
diff --git a/hack/check-python/measure-maintainability-index.sh b/hack/check-python/measure-maintainability-index.sh
new file mode 100755
index 00000000..2abb7e69
--- /dev/null
+++ b/hack/check-python/measure-maintainability-index.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+. ./hack/check-python/prepare-env.sh
+
+[ "$NOVENV" == "1" ] || prepare_venv || exit 1
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+
+pushd "${SCRIPT_DIR}/.."
+radon mi -s -i venv .
+popd
diff --git a/hack/check-python/prepare-env.sh b/hack/check-python/prepare-env.sh
new file mode 100644
index 00000000..ee4c68b0
--- /dev/null
+++ b/hack/check-python/prepare-env.sh
@@ -0,0 +1,12 @@
+
+directories=${directories:-"./test/acceptance/features"}
+pass=0
+fail=0
+
+function prepare_venv() {
+    python3 -m venv venv && source venv/bin/activate
+    for req in $(find . -name 'requirements.txt'); do
+        python3 "$(which pip3)" install -q -r $req;
+    done
+    python3 "$(which pip3)" install -q pydocstyle pyflakes vulture radon
+}
diff --git a/hack/deploy-sbo-local.sh b/hack/deploy-sbo-local.sh
new file mode 100755
index 00000000..edbabd03
--- /dev/null
+++ b/hack/deploy-sbo-local.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+OPERATOR_NAMESPACE=${OPERATOR_NAMESPACE:-}
+ZAP_FLAGS=${ZAP_FLAGS:-}
+
+SBO_LOCAL_LOG=out/sbo-local.log
+
+killall operator-sdk service-binding-operator-local
+
+operator-sdk --verbose run --local --namespace="$OPERATOR_NAMESPACE" --operator-flags "$ZAP_FLAGS" > $SBO_LOCAL_LOG 2>&1 &
+
+SBO_PID=$!
+
+attempts=12
+while [ -z "$(grep 'Starting workers' $SBO_LOCAL_LOG)" ]; do
+    if [[ $attempts -ge 0 ]]; then
+        sleep 5
+        attempts=$((attempts-1))
+    else
+        echo "FAILED"
+        kill $SBO_PID
+        exit 1
+    fi
+done
+
+echo $SBO_PID
diff --git a/hack/deploy-sbo-operator-hub.sh b/hack/deploy-sbo-operator-hub.sh
new file mode 100755
index 00000000..21d66948
--- /dev/null
+++ b/hack/deploy-sbo-operator-hub.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+# FIXME: This is a placeholder for installing SBO via operator hub
+echo "FAILED"
diff --git a/pkg/controller/servicebindingrequest/annotations.go b/pkg/controller/servicebindingrequest/annotations.go
index 12d57750..19521b5b 100644
--- a/pkg/controller/servicebindingrequest/annotations.go
+++ b/pkg/controller/servicebindingrequest/annotations.go
@@ -123,7 +123,7 @@ func setAndUpdateSBRAnnotations(
 ) error {
 	for _, obj := range objs {
 		newObj := setSBRAnnotations(namespacedName, obj)
-		equal, err := nestedMapComparison(obj, newObj, []string{"metadata", "annotations"}...)
+		equal, err := nestedUnstructuredComparison(obj, newObj, []string{"metadata", "annotations"}...)
 		if err != nil {
 			return err
 		}
@@ -154,7 +154,7 @@ func removeSBRAnnotations(obj *unstructured.Unstructured) *unstructured.Unstruct
 func removeAndUpdateSBRAnnotations(client dynamic.Interface, objs []*unstructured.Unstructured) error {
 	for _, obj := range objs {
 		newObj := removeSBRAnnotations(obj)
-		equal, err := nestedMapComparison(obj, newObj, []string{"metadata", "annotations"}...)
+		equal, err := nestedUnstructuredComparison(obj, newObj, []string{"metadata", "annotations"}...)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/controller/servicebindingrequest/binder.go b/pkg/controller/servicebindingrequest/binder.go
index 97eccc61..564b6026 100644
--- a/pkg/controller/servicebindingrequest/binder.go
+++ b/pkg/controller/servicebindingrequest/binder.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -386,10 +385,15 @@ func (b *binder) updateContainer(container interface{}) (map[string]interface{},
 	// effectively binding the application with intermediary secret
 	c.EnvFrom = b.appendEnvFrom(c.EnvFrom, b.sbr.GetName())
 
+	secretRes := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
+	existingSecret, err := b.dynClient.Resource(secretRes).Namespace(b.sbr.GetNamespace()).Get(b.sbr.GetName(), metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
 	// add a special environment variable that is only used to trigger a change in the declaration,
 	// attempting to force a side effect (in case of a Deployment, it would result in its Pods to be
 	// restarted)
-	c.Env = b.appendEnvVar(c.Env, changeTriggerEnv, time.Now().Format(time.RFC3339))
+	c.Env = b.appendEnvVar(c.Env, changeTriggerEnv, existingSecret.GetResourceVersion())
 
 	if len(b.volumeKeys) > 0 {
 		// and adding volume mount entries
@@ -450,8 +454,8 @@ func (b *binder) removeVolumeMounts(volumeMounts []corev1.VolumeMount) []corev1.
 	return cleanVolumeMounts
 }
 
-// nestedMapComparison compares a nested field from two objects.
-func nestedMapComparison(a, b *unstructured.Unstructured, fields ...string) (bool, error) {
+// nestedUnstructuredComparison compares a nested field from two objects.
+func nestedUnstructuredComparison(a, b *unstructured.Unstructured, fields ...string) (bool, error) {
 	var (
 		aMap map[string]interface{}
 		bMap map[string]interface{}
@@ -472,9 +476,14 @@ func nestedMapComparison(a, b *unstructured.Unstructured, fields ...string) (boo
 		return false, nil
 	}
 
-	result := cmp.DeepEqual(aMap, bMap)()
+	result := nestedMapComparison(aMap, bMap)
+	return result, nil
+}
+
+func nestedMapComparison(a, b map[string]interface{}) bool {
+	val := cmp.DeepEqual(a, b)()
+	return val.Success()
 
-	return result.Success(), nil
 }
 
 // update the list of objects informed as unstructured, looking for "containers" entry. This method
@@ -504,7 +513,7 @@ func (b *binder) update(objs *unstructured.UnstructuredList) ([]*unstructured.Un
 			}
 		}
 
-		if specsAreEqual, err := nestedMapComparison(&obj, updatedObj); err != nil {
+		if specsAreEqual, err := nestedUnstructuredComparison(&obj, updatedObj); err != nil {
 			log.Error(err, "")
 			continue
 		} else if specsAreEqual {
diff --git a/pkg/controller/servicebindingrequest/sbrcontroller.go b/pkg/controller/servicebindingrequest/sbrcontroller.go
index 2a47d0d6..d8d6f43d 100644
--- a/pkg/controller/servicebindingrequest/sbrcontroller.go
+++ b/pkg/controller/servicebindingrequest/sbrcontroller.go
@@ -54,7 +54,7 @@ func compareObjectFields(objOld, objNew runtime.Object, fields ...string) (bool,
 		return false, err
 	}
 
-	return nestedMapComparison(
+	return nestedUnstructuredComparison(
 		&unstructured.Unstructured{Object: mapNew},
 		&unstructured.Unstructured{Object: mapOld},
 		fields...,
diff --git a/pkg/controller/servicebindingrequest/secret.go b/pkg/controller/servicebindingrequest/secret.go
index f05af5c7..705c010e 100644
--- a/pkg/controller/servicebindingrequest/secret.go
+++ b/pkg/controller/servicebindingrequest/secret.go
@@ -1,6 +1,9 @@
 package servicebindingrequest
 
 import (
+	"encoding/base64"
+	"reflect"
+
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -47,28 +50,48 @@ func (s *secret) createOrUpdate(payload map[string][]byte, ownerReference metav1
 	resourceClient := s.buildResourceClient()
 
 	logger.Debug("Attempt to create secret...")
-	_, err = resourceClient.Create(u, metav1.CreateOptions{})
-	if err != nil && !errors.IsAlreadyExists(err) {
-		return nil, err
-	}
-
-	logger.Debug("Secret already exists, updating contents instead...")
-	_, err = resourceClient.Update(u, metav1.UpdateOptions{})
+	existingSecret, err := s.get()
 	if err != nil {
+		if errors.IsNotFound(err) {
+			_, err := resourceClient.Create(u, metav1.CreateOptions{})
+			if err != nil {
+				return nil, err
+			}
+			return u, nil
+		}
 		return nil, err
 	}
+	existingSecretData, _, _ := unstructured.NestedMap(existingSecret.Object, "data")
+	payloadStr := make(map[string]string)
+	for k, v := range payload {
+		payloadStr[k] = base64.StdEncoding.EncodeToString(v)
+	}
+	payloadInterim := make(map[string]interface{})
+	for k, v := range payloadStr {
+		payloadInterim[k] = reflect.ValueOf(v).Interface()
+	}
+	eq := nestedMapComparison(existingSecretData, payloadInterim)
+	if eq {
+		logger.Debug("Secret data is same. Skip Update")
+	} else {
+		logger.Debug("Secret data is different. Update Secret")
+		_, err = resourceClient.Update(u, metav1.UpdateOptions{})
+		if err != nil {
+			return nil, err
+		}
+	}
 	return u, nil
 }
 
 // get an unstructured object from the secret handled by this component. It can return errors in case
 // the API server does.
-func (s *secret) get() (*unstructured.Unstructured, bool, error) {
+func (s *secret) get() (*unstructured.Unstructured, error) {
 	resourceClient := s.buildResourceClient()
 	u, err := resourceClient.Get(s.name, metav1.GetOptions{})
-	if err != nil && !errors.IsNotFound(err) {
-		return nil, false, err
+	if err != nil {
+		return nil, err
 	}
-	return u, u != nil, nil
+	return u, nil
 }
 
 // newSecret instantiate a new Secret.
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..6ecd48d4
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[pycodestyle]
+max-line-length = 160
+statistics = True


Test patch:
diff --git a/pkg/controller/servicebindingrequest/annotations_test.go b/pkg/controller/servicebindingrequest/annotations_test.go
index ee459bcd..c6d26d67 100644
--- a/pkg/controller/servicebindingrequest/annotations_test.go
+++ b/pkg/controller/servicebindingrequest/annotations_test.go
@@ -109,11 +109,11 @@ func TestAnnotationsSetAndRemoveSBRAnnotations(t *testing.T) {
 		newObj := setSBRAnnotations(namespacedName, u)
 
 		// we are not modifying the origin object
-		equal, err := nestedMapComparison(u, originCopy)
+		equal, err := nestedUnstructuredComparison(u, originCopy)
 		require.NoError(t, err)
 		require.True(t, equal)
 
-		equal, err = nestedMapComparison(u, newObj, []string{"metadata", "annotations"}...)
+		equal, err = nestedUnstructuredComparison(u, newObj, []string{"metadata", "annotations"}...)
 		require.NoError(t, err)
 		require.False(t, equal)
 
@@ -123,7 +123,7 @@ func TestAnnotationsSetAndRemoveSBRAnnotations(t *testing.T) {
 
 		// assert nothing else is changed
 		newObj.SetAnnotations(nil)
-		equal, err = nestedMapComparison(u, newObj)
+		equal, err = nestedUnstructuredComparison(u, newObj)
 		require.NoError(t, err)
 		require.True(t, equal)
 	})
@@ -133,11 +133,11 @@ func TestAnnotationsSetAndRemoveSBRAnnotations(t *testing.T) {
 		newObj := removeSBRAnnotations(u)
 
 		// we are not modifying the origin object
-		equal, err := nestedMapComparison(u, originCopy)
+		equal, err := nestedUnstructuredComparison(u, originCopy)
 		require.NoError(t, err)
 		require.True(t, equal)
 
-		equal, err = nestedMapComparison(u, newObj, []string{"metadata", "annotations"}...)
+		equal, err = nestedUnstructuredComparison(u, newObj, []string{"metadata", "annotations"}...)
 		require.NoError(t, err)
 		require.False(t, equal)
 
@@ -147,7 +147,7 @@ func TestAnnotationsSetAndRemoveSBRAnnotations(t *testing.T) {
 
 		// assert nothing else is changed
 		newObj.SetAnnotations(u.GetAnnotations())
-		equal, err = nestedMapComparison(u, newObj)
+		equal, err = nestedUnstructuredComparison(u, newObj)
 		require.NoError(t, err)
 		require.True(t, equal)
 	})
diff --git a/pkg/controller/servicebindingrequest/binder_test.go b/pkg/controller/servicebindingrequest/binder_test.go
index fdd6579a..f0c98771 100644
--- a/pkg/controller/servicebindingrequest/binder_test.go
+++ b/pkg/controller/servicebindingrequest/binder_test.go
@@ -4,13 +4,13 @@ import (
 	"context"
 	"fmt"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
@@ -56,6 +56,9 @@ func TestBinderNew(t *testing.T) {
 		map[string]string{},
 	)
 
+	f.AddMockedUnstructuredSecretRV(name)
+	fakeDynClient := f.FakeDynClient()
+
 	t.Run("search-using-resourceref", func(t *testing.T) {
 		binderForSBRWithResourceRef := newBinder(
 			context.TODO(),
@@ -170,9 +173,12 @@ func TestBinderNew(t *testing.T) {
 		require.NotNil(t, envVar)
 		require.NotEmpty(t, envVar.Value)
 
-		parsedTime, err := time.Parse(time.RFC3339, envVar.Value)
+		uSecret, err := fakeDynClient.Resource(secretsGVR).Get(name, metav1.GetOptions{})
+		require.NoError(t, err)
+		s := corev1.Secret{}
+		err = runtime.DefaultUnstructuredConverter.FromUnstructured(uSecret.Object, &s)
 		require.NoError(t, err)
-		require.True(t, parsedTime.Before(time.Now()))
+		require.Equal(t, s.ObjectMeta.ResourceVersion, envVar.Value)
 
 	})
 
diff --git a/pkg/controller/servicebindingrequest/reconciler_test.go b/pkg/controller/servicebindingrequest/reconciler_test.go
index 0509a9c9..fc1fd7de 100644
--- a/pkg/controller/servicebindingrequest/reconciler_test.go
+++ b/pkg/controller/servicebindingrequest/reconciler_test.go
@@ -303,7 +303,6 @@ func TestApplicationNotFound(t *testing.T) {
 	d := appsv1.Deployment{}
 	err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
 	require.NoError(t, err)
-
 	sbrOutput2, err := r.getServiceBindingRequest(namespacedName)
 	require.NoError(t, err)
 
@@ -312,8 +311,28 @@ func TestApplicationNotFound(t *testing.T) {
 
 	require.Equal(t, reconcilerName, sbrOutput2.Status.Secret)
 	require.Equal(t, 1, len(sbrOutput2.Status.Applications))
+}
 
-	u, err = fakeDynClient.Resource(secretsGVR).Get("db-credentials", metav1.GetOptions{})
+func TestReconcilerUpdateCredentials(t *testing.T) {
+	backingServiceResourceRef := "backingService"
+	matchLabels := map[string]string{
+		"connects-to": "database",
+		"environment": "reconciler",
+	}
+	f := mocks.NewFake(t, reconcilerNs)
+	f.AddMockedUnstructuredServiceBindingRequest(reconcilerName, backingServiceResourceRef, "", deploymentsGVR, matchLabels)
+	f.AddMockedUnstructuredCSV("cluster-service-version-list")
+	f.AddMockedUnstructuredDatabaseCRD()
+	f.AddMockedUnstructuredDatabaseCR(backingServiceResourceRef)
+	f.AddMockedUnstructuredSecret("db-credentials")
+	f.AddMockedUnstructuredDeployment(reconcilerName, matchLabels)
+
+	fakeDynClient := f.FakeDynClient()
+	mapper := testutils.BuildTestRESTMapper()
+	r := &reconciler{dynClient: fakeDynClient, restMapper: mapper, scheme: f.S}
+	r.resourceWatcher = newFakeResourceWatcher(mapper)
+
+	u, err := fakeDynClient.Resource(secretsGVR).Get("db-credentials", metav1.GetOptions{})
 	require.NoError(t, err)
 	s := corev1.Secret{}
 	err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &s)
@@ -321,34 +340,35 @@ func TestApplicationNotFound(t *testing.T) {
 
 	// Update Credentials
 	s.Data["password"] = []byte("abc123")
+	// Update resourceVersion for postgresdb
+	s.ObjectMeta.ResourceVersion = "112200"
 	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&s)
 	require.NoError(t, err)
 	updated := unstructured.Unstructured{Object: obj}
 	_, err = fakeDynClient.Resource(secretsGVR).Namespace(updated.GetNamespace()).Update(&updated, metav1.UpdateOptions{})
 	require.NoError(t, err)
+
 	time.Sleep(1 * time.Second)
-	r = &reconciler{dynClient: fakeDynClient, restMapper: testutils.BuildTestRESTMapper(), scheme: f.S}
-	r.resourceWatcher = newFakeResourceWatcher(mapper)
-	res, err = r.Reconcile(reconcileRequest())
+	res, err := r.Reconcile(reconcileRequest())
 	require.NoError(t, err)
 	require.False(t, res.Requeue)
 
+	namespacedName := types.NamespacedName{Namespace: reconcilerNs, Name: reconcilerName}
 	sbrOutput3, err := r.getServiceBindingRequest(namespacedName)
 	require.NoError(t, err)
 
-	u, err = fakeDynClient.Resource(deploymentsGVR).Get(reconcilerName, metav1.GetOptions{})
-	require.NoError(t, err)
-
-	d = appsv1.Deployment{}
-	err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &d)
-	require.NoError(t, err)
-
 	requireConditionPresentAndTrue(t, CollectionReady, sbrOutput3.Status.Conditions)
 	requireConditionPresentAndTrue(t, InjectionReady, sbrOutput3.Status.Conditions)
 
 	require.Equal(t, reconcilerName, sbrOutput3.Status.Secret)
 	require.Equal(t, s.Data["password"], []byte("abc123"))
 	require.Equal(t, 1, len(sbrOutput3.Status.Applications))
+	require.Equal(t, reconcilerName, sbrOutput3.Status.Secret)
+	fetchSecret, err := fakeDynClient.Resource(secretsGVR).Namespace(updated.GetNamespace()).Get(reconcilerName, metav1.GetOptions{})
+	require.NoError(t, err)
+	require.Equal(t, fetchSecret.GetName(), reconcilerName)
+	require.Equal(t, fetchSecret.GetResourceVersion(), "")
+	require.Equal(t, 1, len(sbrOutput3.Status.Applications))
 }
 
 //TestReconcilerReconcileWithConflictingAppSelc tests when sbr has conflicting ApplicationSel such as MatchLabels=App1 and ResourceRef=App2 it should prioritise the ResourceRef
diff --git a/pkg/controller/servicebindingrequest/secret_test.go b/pkg/controller/servicebindingrequest/secret_test.go
index ba7c4934..e9e5e678 100644
--- a/pkg/controller/servicebindingrequest/secret_test.go
+++ b/pkg/controller/servicebindingrequest/secret_test.go
@@ -47,9 +47,8 @@ func TestSecretNew(t *testing.T) {
 	})
 
 	t.Run("Get", func(t *testing.T) {
-		u, found, err := s.get()
+		u, err := s.get()
 		assert.NoError(t, err)
-		assert.True(t, found)
 		assertSecretNamespacedName(t, u, ns, name)
 	})
 }
diff --git a/test/acceptance/README.md b/test/acceptance/README.md
new file mode 100644
index 00000000..0d4eaf0f
--- /dev/null
+++ b/test/acceptance/README.md
@@ -0,0 +1,10 @@
+# How to execute the script
+
+## Install python dependencies
+pip install -r test/acceptance/features/requirements.txt
+
+## Use this make target to check the test/acceptance python behave framework is a clean code
+make lint-python-code
+
+## Use this command to run the tests
+make test-acceptance
\ No newline at end of file
diff --git a/test/acceptance/features/bindToService.feature b/test/acceptance/features/bindToService.feature
new file mode 100644
index 00000000..cff3df40
--- /dev/null
+++ b/test/acceptance/features/bindToService.feature
@@ -0,0 +1,218 @@
+Feature: Bind an application to a service
+
+    As a user of Service Binding Operator
+    I want to bind applications to services it depends on
+
+    Background:
+        Given Namespace [TEST_NAMESPACE] is used
+        * Service Binding Operator is running
+        * PostgreSQL DB operator is installed
+
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: Application, DB and Service Binding Request
+        Given Imported Nodejs application "nodejs-rest-http-crud-a-d-s" is running
+        * DB "db-demo-a-d-s" is running
+        When Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-a-d-s
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-a-d-s
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-a-d-s
+            """
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-a-d-s"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-a-d-s" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-a-d-s" should be changed to "True"
+
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: Application, Service Binding Request and DB
+        Given Imported Nodejs application "nodejs-rest-http-crud-a-s-d" is running
+        * Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-a-s-d
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-a-s-d
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-a-s-d
+            """
+        When DB "db-demo-a-s-d" is running
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-a-s-d"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-a-s-d" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-a-s-d" should be changed to "True"
+
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: DB, Application and Service Binding Request
+        Given DB "db-demo-d-a-s" is running
+        * Imported Nodejs application "nodejs-rest-http-crud-d-a-s" is running
+        When Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-d-a-s
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-d-a-s
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-d-a-s
+            """
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-d-a-s"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-d-a-s" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-d-a-s" should be changed to "True"
+
+    # Currently disabled as not supported by SBO
+    @disabled
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: DB, Service Binding Request and Application
+        Given DB "db-demo-d-s-a" is running
+        * Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-d-s-a
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-d-s-a
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-d-s-a
+            """
+        When Imported Nodejs application "nodejs-rest-http-crud-d-s-a" is running
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-d-s-a"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-d-s-a" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-d-s-a" should be changed to "True"
+
+    # Currently disabled as not supported by SBO
+    @disabled
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: Service Binding Request, Application and DB
+        Given Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-s-a-d
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-s-a-d
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-s-a-d
+            """
+        * Imported Nodejs application "nodejs-rest-http-crud-s-a-d" is running
+        When DB "db-demo-s-a-d" is running
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-s-a-d"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-s-a-d" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-s-a-d" should be changed to "True"
+
+    # Currently disabled as not supported by SBO
+    @disabled
+    Scenario: Bind an imported Node.js application to PostgreSQL database in the following order: Service Binding Request, DB and Application
+        Given Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-s-d-a
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-rest-http-crud-s-d-a
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-s-d-a
+            """
+        * DB "db-demo-s-d-a" is running
+        When Imported Nodejs application "nodejs-rest-http-crud-s-d-a" is running
+        Then application should be re-deployed
+        And application should be connected to the DB "db-demo-s-d-a"
+        And jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-s-d-a" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-s-d-a" should be changed to "True"
+
+    @negative
+    Scenario: Attempt to bind a non existing application to PostgreSQL database
+        Given DB "db-demo-missing-app" is running
+        * Imported Nodejs application "nodejs-missing-app" is not running
+        When Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-missing-app
+            spec:
+                applicationSelector:
+                    resourceRef: nodejs-missing-app
+                    group: apps
+                    version: v1
+                    resource: deployments
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-missing-app
+            """
+        Then jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-missing-app" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-missing-app" should be changed to "False"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").reason" of Service Binding Request "binding-request-missing-app" should be changed to "ApplicationNotFound"
+
+    @negative
+    Scenario: Service Binding Request without application selector
+        Given Imported Nodejs application "nodejs-empty-app" is running
+        And DB "db-demo-empty-app" is running
+        When Service Binding Request is applied to connect the database and the application
+            """
+            apiVersion: apps.openshift.io/v1alpha1
+            kind: ServiceBindingRequest
+            metadata:
+                name: binding-request-empty-app
+            spec:
+                backingServiceSelector:
+                    group: postgresql.baiju.dev
+                    version: v1alpha1
+                    kind: Database
+                    resourceRef: db-demo-empty-app
+            """
+        Then jq ".status.conditions[] | select(.type=="CollectionReady").status" of Service Binding Request "binding-request-empty-app" should be changed to "True"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").status" of Service Binding Request "binding-request-empty-app" should be changed to "False"
+        And jq ".status.conditions[] | select(.type=="InjectionReady").reason" of Service Binding Request "binding-request-empty-app" should be changed to "EmptyApplicationSelector"
diff --git a/test/acceptance/features/environment.py b/test/acceptance/features/environment.py
new file mode 100644
index 00000000..19d40560
--- /dev/null
+++ b/test/acceptance/features/environment.py
@@ -0,0 +1,25 @@
+"""
+before_step(context, step), after_step(context, step)
+    These run before and after every step.
+    The step passed in is an instance of Step.
+before_scenario(context, scenario), after_scenario(context, scenario)
+    These run before and after each scenario is run.
+    The scenario passed in is an instance of Scenario.
+before_feature(context, feature), after_feature(context, feature)
+    These run before and after each feature file is exercised.
+    The feature passed in is an instance of Feature.
+before_all(context), after_all(context)
+    These run before and after the whole shooting match.
+"""
+
+import subprocess
+from pyshould import should
+
+
+def before_scenario(_context, _scenario):
+    print("Getting OC status before {} scenario".format(_scenario))
+    code, output = subprocess.getstatusoutput('oc get project default')
+    print("[CODE] {}".format(code))
+    print("[CMD] {}".format(output))
+    code | should.be_equal_to(0)
+    print("***Connected to cluster***")
diff --git a/test/acceptance/features/requirements.txt b/test/acceptance/features/requirements.txt
new file mode 100644
index 00000000..200dd264
--- /dev/null
+++ b/test/acceptance/features/requirements.txt
@@ -0,0 +1,3 @@
+behave
+pyshould
+requests
\ No newline at end of file
diff --git a/test/acceptance/features/steps/command.py b/test/acceptance/features/steps/command.py
new file mode 100644
index 00000000..3af3f439
--- /dev/null
+++ b/test/acceptance/features/steps/command.py
@@ -0,0 +1,48 @@
+import subprocess
+import time
+import os
+
+
+class Command(object):
+    path = ""
+    env = {}
+
+    def __init__(self, path=None):
+        if path is None:
+            self.path = os.getcwd()
+        else:
+            self.path = path
+
+        self.setenv("KUBECONFIG", os.getenv("KUBECONFIG"))
+        self.setenv("PATH", os.getenv("PATH"))
+
+    def setenv(self, key, value):
+        self.env[key] = value
+
+    def run(self, cmd, stdin=None):
+        output = None
+        exit_code = 0
+        try:
+            if stdin is None:
+                output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, cwd=self.path, env=self.env)
+            else:
+                output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, cwd=self.path, env=self.env, input=stdin.encode("utf-8"))
+        except subprocess.CalledProcessError as err:
+            output = err.output
+            exit_code = err.returncode
+            print('ERROR MESSGE:', output)
+            print('ERROR CODE:', exit_code)
+        return output.decode("utf-8"), exit_code
+
+    def run_wait_for_status(self, cmd, status=None, interval=10, timeout=60):
+        start = 0
+        if status is not None:
+            while ((start + interval) <= timeout):
+                cmd_output, exit_code = self.run(cmd)
+                if status in cmd_output:
+                    return True, cmd_output, exit_code
+                time.sleep(interval)
+                start += interval
+
+        print("ERROR: Time out while waiting for status message.")
+        return False, self.run(cmd)
diff --git a/test/acceptance/features/steps/dboperator.py b/test/acceptance/features/steps/dboperator.py
new file mode 100644
index 00000000..01187d3e
--- /dev/null
+++ b/test/acceptance/features/steps/dboperator.py
@@ -0,0 +1,55 @@
+import re
+from pyshould import should, should_not
+
+from command import Command
+from openshift import Openshift
+
+
+class DbOperator():
+
+    openshift = Openshift()
+    cmd = Command()
+
+    pod_name_pattern = "{name}.*"
+
+    name = ""
+    namespace = ""
+    operator_source_name = "db-operators"
+    operator_registry_namespace = "pmacik"
+    operator_registry_channel = "stable"
+    package_name = "db-operators"
+
+    def __init__(self, name="postgresql-operator", namespace="openshift-operators"):
+        self.name = name
+        self.namespace = namespace
+
+    def is_running(self, wait=False):
+        if wait:
+            pod_name = self.openshift.wait_for_pod(self.pod_name_pattern.format(name=self.name), self.namespace)
+        else:
+            pod_name = self.openshift.search_pod_in_namespace(self.pod_name_pattern.format(name=self.name), self.namespace)
+        if pod_name is not None:
+            operator_pod_status = self.openshift.check_pod_status(pod_name, self.namespace)
+            print("The pod {} is running: {}".format(self.name, operator_pod_status))
+            return operator_pod_status
+        else:
+            return False
+
+    def install_operator_source(self):
+        install_src_output = self.openshift.create_operator_source(self.operator_source_name, self.operator_registry_namespace)
+        if not re.search(r'.*operatorsource.operators.coreos.com/%s\s(unchanged|created)' % self.operator_source_name, install_src_output):
+            print("Failed to create {} operator source".format(self.operator_source_name))
+            return False
+        return self.openshift.wait_for_package_manifest(self.package_name, self.operator_source_name, self.operator_registry_channel)
+
+    def install_operator_subscription(self):
+        install_sub_output = self.openshift.create_operator_subscription(self.package_name, self.operator_source_name, self.operator_registry_channel)
+        return re.search(r'.*subscription.operators.coreos.com/%s\s(unchanged|created)' % self.operator_source_name, install_sub_output)
+
+    def get_package_manifest(self):
+        cmd = 'oc get packagemanifest %s -o "jsonpath={.metadata.name}"' % self.pkgManifest
+        manifest = self.cmd.run_check_for_status(
+            cmd, status=self.pkgManifest)
+        manifest | should_not.be_equal_to(None)
+        manifest | should.equal(self.pkgManifest)
+        return manifest
diff --git a/test/acceptance/features/steps/namespace.py b/test/acceptance/features/steps/namespace.py
new file mode 100644
index 00000000..ec2e3905
--- /dev/null
+++ b/test/acceptance/features/steps/namespace.py
@@ -0,0 +1,33 @@
+import re
+
+from command import Command
+
+
+class Namespace():
+    def __init__(self, name):
+        self.name = name
+        self.cmd = Command()
+
+    def create(self):
+        create_namespace_output, exit_code = self.cmd.run("oc new-project {}".format(self.name))
+        if re.search(r'Now using project \"%s\"\son\sserver' % self.name, create_namespace_output) or \
+                re.search(r'.*Already\son\sproject\s\"%s\"\son\sserver.*' % self.name, create_namespace_output):
+            return True
+        elif re.search(r'.*project.project.openshift.io\s\"%s\"\salready exists' % self.name, create_namespace_output):
+            return self.switch_to()
+        else:
+            print("Returned a different value {}".format(create_namespace_output))
+        return False
+
+    def is_present(self):
+        output, exit_code = self.cmd.run('oc get ns {}'.format(self.name))
+        return exit_code == 0
+
+    def switch_to(self):
+        create_namespace_output, exit_code = self.cmd.run('oc project {}'.format(self.name))
+        if re.search(r'Now using project \"%s\"\son\sserver' % self.name, create_namespace_output):
+            return True
+        elif re.search(r'.*Already\son\sproject\s\"%s\"\son\sserver.*' % self.name, create_namespace_output):
+            return True
+        else:
+            return False
diff --git a/test/acceptance/features/steps/nodejs_application.py b/test/acceptance/features/steps/nodejs_application.py
new file mode 100644
index 00000000..779ee980
--- /dev/null
+++ b/test/acceptance/features/steps/nodejs_application.py
@@ -0,0 +1,105 @@
+from openshift import Openshift
+from command import Command
+import re
+import requests
+import time
+
+
+class NodeJSApp(object):
+
+    nodejs_app = "https://github.com/pmacik/nodejs-rest-http-crud"
+    api_end_point = 'http://{route_url}/api/status/dbNameCM'
+    openshift = Openshift()
+
+    pod_name_pattern = "{name}.*$(?<!-build)"
+
+    name = ""
+    namespace = ""
+
+    def __init__(self, name, namespace):
+        self.cmd = Command()
+        self.name = name
+        self.namespace = namespace
+
+    def is_running(self, wait=False):
+        deployment_flag = False
+
+        if wait:
+            pod_name = self.openshift.wait_for_pod(self.get_pod_name_pattern(), self.namespace, timeout=180)
+        else:
+            pod_name = self.openshift.search_pod_in_namespace(self.get_pod_name_pattern(), self.namespace)
+
+        if pod_name is not None:
+            application_pod_status = self.openshift.check_pod_status(pod_name, self.namespace, wait_for_status="Running")
+            print("The pod {} is running: {}".format(pod_name, application_pod_status))
+
+            deployment = self.openshift.search_resource_in_namespace("deployments", f"{self.name}.*", self.namespace)
+            if deployment is not None:
+                print("deployment is {}".format(deployment))
+                deployment_flag = True
+
+            if application_pod_status and deployment_flag:
+                return True
+            else:
+                return False
+        else:
+            return False
+
+    def install(self):
+        nodejs_app_arg = "nodejs~" + self.nodejs_app
+        cmd = f"oc new-app {nodejs_app_arg} --name {self.name} -n {self.namespace}"
+        (create_new_app_output, exit_code) = self.cmd.run(cmd)
+        if exit_code != 0:
+            return False
+        for pattern in [f'imagestream.image.openshift.io\\s\"{self.name}\"\\screated',
+                        f'deployment.apps\\s\"{self.name}\"\\screated',
+                        f'service\\s\"{self.name}\"\\screated']:
+            if not re.search(pattern, create_new_app_output):
+                return False
+        if not self.openshift.expose_service_route(self.name, self.namespace):
+            print("Unable to expose the service with build config")
+            return False
+        return True
+
+    def get_db_name_from_api(self, interval=5, timeout=20):
+        route_url = self.openshift.get_route_host(self.name, self.namespace)
+        if route_url is None:
+            return None
+        start = 0
+        while ((start + interval) <= timeout):
+            db_name = requests.get(url=self.api_end_point.format(route_url=route_url))
+            if db_name.status_code == 200:
+                return db_name.text
+            time.sleep(interval)
+            start += interval
+        return None
+
+    def get_observed_generation(self):
+        return self.openshift.get_resource_info_by_jsonpath("deployment", self.name, self.namespace, "{.status.observedGeneration}")
+
+    def get_running_pod_name(self, interval=5, timeout=60):
+        start = 0
+        while ((start + interval) <= timeout):
+            pod_list = self.openshift.get_pod_lst(self.namespace)
+            for pod in pod_list.split(" "):
+                if re.fullmatch(self.get_pod_name_pattern(), pod):
+                    if self.openshift.get_pod_status(pod, self.namespace) == "Running":
+                        return pod
+            time.sleep(interval)
+            start += interval
+        return None
+
+    def get_redeployed_pod_name(self, old_pod_name, interval=5, timeout=60):
+        start = 0
+        while ((start + interval) <= timeout):
+            pod_list = self.openshift.get_pod_lst(self.namespace)
+            for pod in pod_list.split(" "):
+                if pod != old_pod_name and re.fullmatch(self.get_pod_name_pattern(), pod):
+                    if self.openshift.get_pod_status(pod, self.namespace) == "Running":
+                        return pod
+            time.sleep(interval)
+            start += interval
+        return None
+
+    def get_pod_name_pattern(self):
+        return self.pod_name_pattern.format(name=self.name)
diff --git a/test/acceptance/features/steps/openshift.py b/test/acceptance/features/steps/openshift.py
new file mode 100644
index 00000000..10bafe93
--- /dev/null
+++ b/test/acceptance/features/steps/openshift.py
@@ -0,0 +1,190 @@
+import re
+import time
+from pyshould import should
+from command import Command
+
+nodejs_app = "https://github.com/pmacik/nodejs-rest-http-crud"
+
+
+class Openshift(object):
+    def __init__(self):
+        self.cmd = Command()
+        self.operator_source_yaml_template = '''
+---
+apiVersion: operators.coreos.com/v1
+kind: OperatorSource
+metadata:
+    name: {name}
+    namespace: openshift-marketplace
+spec:
+    type: appregistry
+    endpoint: https://quay.io/cnr
+    registryNamespace: {registry_namespace}
+'''
+        self.operator_subscription_yaml_template = '''
+---
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+    name: '{name}'
+    namespace: openshift-operators
+spec:
+    channel: '{channel}' # the quotes are necessary to avoid conversions from strings like '1.0' to be converted to actual decimal numbers
+    installPlanApproval: Automatic
+    name: '{name}'
+    source: '{operator_source_name}'
+    sourceNamespace: openshift-marketplace
+    startingCSV: '{csv_version}'
+'''
+
+    def get_pod_lst(self, namespace):
+        return self.get_resource_lst("pods", namespace)
+
+    def get_resource_lst(self, resource_plural, namespace):
+        (output, exit_code) = self.cmd.run(f'oc get {resource_plural} -n {namespace} -o "jsonpath={{.items[*].metadata.name}}"')
+        exit_code | should.be_equal_to(0)
+        return output
+
+    def search_item_in_lst(self, lst, search_pattern):
+        lst_arr = lst.split(" ")
+        for item in lst_arr:
+            if re.fullmatch(search_pattern, item) is not None:
+                print(f"item matched {item}")
+                return item
+        print("Given item not matched from the list of pods")
+        return None
+
+    def search_pod_in_namespace(self, pod_name_pattern, namespace):
+        return self.search_resource_in_namespace("pods", pod_name_pattern, namespace)
+
+    def search_resource_in_namespace(self, resource_plural, name_pattern, namespace):
+        print(f"Searching for {resource_plural} that matches {name_pattern} in {namespace} namespace")
+        lst = self.get_resource_lst(resource_plural, namespace)
+        if len(lst) != 0:
+            print("Resource list is {}".format(lst))
+            return self.search_item_in_lst(lst, name_pattern)
+        else:
+            print('Resource list is empty under namespace - {}'.format(namespace))
+            return None
+
+    def is_resource_in(self, resource_type):
+        output, exit_code = self.cmd.run(f'oc get {resource_type}')
+        return exit_code == 0
+
+    def wait_for_pod(self, pod_name_pattern, namespace, interval=5, timeout=60):
+        pod = self.search_pod_in_namespace(pod_name_pattern, namespace)
+        start = 0
+        if pod is not None:
+            return pod
+        else:
+            while ((start + interval) <= timeout):
+                pod = self.search_pod_in_namespace(pod_name_pattern, namespace)
+                if pod is not None:
+                    return pod
+                time.sleep(interval)
+                start += interval
+        return None
+
+    def check_pod_status(self, pod_name, namespace, wait_for_status="Running"):
+        cmd = f'oc get pod {pod_name} -n {namespace} -o "jsonpath={{.status.phase}}"'
+        status_found, output, exit_status = self.cmd.run_wait_for_status(cmd, wait_for_status)
+        return status_found
+
+    def get_pod_status(self, pod_name, namespace):
+        cmd = f'oc get pod {pod_name} -n {namespace} -o "jsonpath={{.status.phase}}"'
+        output, exit_status = self.cmd.run(cmd)
+        print(f"Get pod status: {output}, {exit_status}")
+        if exit_status == 0:
+            return output
+        return None
+
+    def oc_apply(self, yaml):
+        (output, exit_code) = self.cmd.run("oc apply -f -", yaml)
+        return output
+
+    def create_operator_source(self, name, registry_namespace):
+        operator_source = self.operator_source_yaml_template.format(name=name, registry_namespace=registry_namespace)
+        return self.oc_apply(operator_source)
+
+    def get_current_csv(self, package_name, catalog, channel):
+        cmd = f'oc get packagemanifests -o json | jq -r \'.items[] \
+            | select(.metadata.name=="{package_name}") \
+            | select(.status.catalogSource=="{catalog}").status.channels[] \
+            | select(.name=="{channel}").currentCSV\''
+        current_csv, exit_code = self.cmd.run(cmd)
+
+        if current_csv is None:
+            return current_csv
+
+        current_csv = current_csv.strip("\n")
+        if current_csv == "" or exit_code != 0:
+            current_csv = None
+        return current_csv
+
+    def create_operator_subscription(self, package_name, operator_source_name, channel):
+        operator_subscription = self.operator_subscription_yaml_template.format(
+            name=package_name, operator_source_name=operator_source_name,
+            channel=channel, csv_version=self.get_current_csv(package_name, operator_source_name, channel))
+        return self.oc_apply(operator_subscription)
+
+    def wait_for_package_manifest(self, package_name, operator_source_name, operator_channel, interval=5, timeout=120):
+        current_csv = self.get_current_csv(package_name, operator_source_name, operator_channel)
+        start = 0
+        if current_csv is not None:
+            return True
+        else:
+            while ((start + interval) <= timeout):
+                current_csv = self.get_current_csv(package_name, operator_source_name, operator_channel)
+                if current_csv is not None:
+                    return True
+                time.sleep(interval)
+                start += interval
+        return False
+
+    def expose_service_route(self, service_name, namespace):
+        output, exit_code = self.cmd.run(f'oc expose svc/{service_name} -n {namespace} --name={service_name}')
+        return re.search(r'.*%s\sexposed' % service_name, output)
+
+    def get_route_host(self, name, namespace):
+        (output, exit_code) = self.cmd.run(f'oc get route {name} -n {namespace} -o "jsonpath={{.status.ingress[0].host}}"')
+        exit_code | should.be_equal_to(0)
+        return output
+
+    def check_for_deployment_status(self, deployment_name, namespace, wait_for_status="True"):
+        deployment_status_cmd = f'oc get deployment {deployment_name} -n {namespace} -o "jsonpath={{.status.conditions[*].status}}"'
+        deployment_status, exit_code = self.cmd.run_wait_for_status(deployment_status_cmd, wait_for_status, 5, 300)
+        exit_code | should.be_equal_to(0)
+        return deployment_status
+
+    def get_deployment_env_info(self, name, namespace):
+        env_cmd = f'oc get deploy {name} -n {namespace} "jsonpath={{.spec.template.spec.containers[0].env}}"'
+        env, exit_code = self.cmd.run(env_cmd)
+        exit_code | should.be_equal_to(0)
+        env_from_cmd = f'oc get deploy {name} -n {namespace} "jsonpath={{.spec.template.spec.containers[0].envFrom}}"'
+        env_from, exit_code = self.cmd.run(env_from_cmd)
+        exit_code | should.be_equal_to(0)
+        return env, env_from
+
+    def get_resource_info_by_jsonpath(self, resource_type, name, namespace, json_path, wait=False):
+        output, exit_code = self.cmd.run(f'oc get {resource_type} {name} -n {namespace} -o "jsonpath={json_path}"')
+        if exit_code != 0:
+            if wait:
+                attempts = 5
+                while exit_code != 0 and attempts > 0:
+                    output, exit_code = self.cmd.run(f'oc get {resource_type} {name} -n {namespace} -o "jsonpath={json_path}"')
+                    attempts -= 1
+                    time.sleep(5)
+        exit_code | should.be_equal_to(0).desc(f'Exit code should be 0:\n OUTPUT:\n{output}')
+        return output
+
+    def get_resource_info_by_jq(self, resource_type, name, namespace, jq_expression, wait=False):
+        output, exit_code = self.cmd.run(f'oc get {resource_type} {name} -n {namespace} -o json | jq -rc \'{jq_expression}\'')
+        if exit_code != 0:
+            if wait:
+                attempts = 5
+                while exit_code != 0 and attempts > 0:
+                    output, exit_code = self.cmd.run(f'oc get {resource_type} {name} -n {namespace} -o json | jq -rc \'{jq_expression}\'')
+                    attempts -= 1
+                    time.sleep(5)
+        exit_code | should.be_equal_to(0).desc(f'Exit code should be 0:\n OUTPUT:\n{output}')
+        return output.rstrip("\n")
diff --git a/test/acceptance/features/steps/postgres_db.py b/test/acceptance/features/steps/postgres_db.py
new file mode 100644
index 00000000..37118953
--- /dev/null
+++ b/test/acceptance/features/steps/postgres_db.py
@@ -0,0 +1,60 @@
+import re
+
+from command import Command
+from openshift import Openshift
+
+
+class PostgresDB(object):
+
+    cmd = Command()
+    openshift = Openshift()
+
+    pod_name_pattern = "{name}.*"
+
+    name = ""
+    namespace = ""
+
+    database_yaml_template = """---
+apiVersion: postgresql.baiju.dev/v1alpha1
+kind: Database
+metadata:
+  name: {name}
+  namespace: {namespace}
+spec:
+  image: docker.io/postgres
+  imageName: postgres
+  dbName: {name}
+"""
+
+    def __init__(self, name, namespace):
+        self.name = name
+        self.namespace = namespace
+
+    def create(self):
+        db_create_output = self.openshift.oc_apply(self.database_yaml_template.format(name=self.name, namespace=self.namespace))
+        return re.search(r'.*database.postgresql.baiju.dev/%s\s(created|unchanged)' % self.name, db_create_output)
+
+    def is_running(self, wait=False):
+        if wait:
+            pod_name = self.openshift.wait_for_pod(self.pod_name_pattern.format(name=self.name), self.namespace, timeout=120)
+        else:
+            pod_name = self.openshift.search_pod_in_namespace(self.pod_name_pattern.format(name=self.name), self.namespace)
+        if pod_name is not None:
+            pod_status = self.openshift.check_pod_status(pod_name, self.namespace)
+            print("The pod {} is running: {}".format(self.name, pod_status))
+            output, exit_code = self.cmd.run(f'oc get db {self.name} -n {self.namespace} -o jsonpath="{{.status.dbConnectionIP}}"')
+            if exit_code == 0 and re.search(r'\d+\.\d+\.\d+\.\d+', output):
+                print(f"The DB {self.name} is up and listening at {output}.")
+                return True
+        return False
+
+    def get_db_instance_name(self, project):
+        cmd = 'oc get db -n %s -o "jsonpath={.items[*].metadata.name}"' % project
+        return self.cmd.run(cmd)
+
+    def get_connection_ip(self):
+        cmd = 'oc get db %s -o "jsonpath={.status.dbConnectionIP}"' % self.db_instance
+        return self.cmd.run(cmd)
+
+    def check_pod_status(self, status="Running"):
+        return self.openshift.check_pod_status(self.name, self.namespace, wait_for_status=status)
diff --git a/test/acceptance/features/steps/service_binding_request.py b/test/acceptance/features/steps/service_binding_request.py
new file mode 100644
index 00000000..4cb32f2a
--- /dev/null
+++ b/test/acceptance/features/steps/service_binding_request.py
@@ -0,0 +1,12 @@
+import re
+
+from openshift import Openshift
+
+
+class ServiceBindingRequest(object):
+
+    openshift = Openshift()
+
+    def create(self, yaml):
+        sbr_create_output = self.openshift.oc_apply(yaml)
+        return re.search(r'.*servicebindingrequest.apps.openshift.io/.*(created|unchanged)', sbr_create_output)
diff --git a/test/acceptance/features/steps/servicebindingoperator.py b/test/acceptance/features/steps/servicebindingoperator.py
new file mode 100644
index 00000000..85fe2515
--- /dev/null
+++ b/test/acceptance/features/steps/servicebindingoperator.py
@@ -0,0 +1,33 @@
+import os
+
+from openshift import Openshift
+from pyshould import should, should_not
+
+
+class Servicebindingoperator():
+    openshift = Openshift()
+    name = ""
+    namespace = ""
+
+    def __init__(self,  name="service-binding-operator", namespace="openshift-operators"):
+        self.namespace = namespace
+        self.name = name
+
+    def check_resources(self):
+        self.openshift.is_resource_in("servicebindingrequest") | should.be_truthy.desc("CRD is in")
+        self.openshift.search_resource_in_namespace("rolebindings", self.name, self.namespace) | should_not.be_none.desc("Role binding is in")
+        self.openshift.search_resource_in_namespace("roles", self.name, self.namespace) | should_not.be_none.desc("Role is in")
+        self.openshift.search_resource_in_namespace("serviceaccounts", self.name, self.namespace) | should_not.be_none.desc("Service Account")
+        return True
+
+    def is_running(self):
+        start_sbo = os.getenv("TEST_ACCEPTANCE_START_SBO")
+        start_sbo | should_not.be_none.desc("TEST_ACCEPTANCE_START_SBO is set")
+        start_sbo | should.be_in({"local", "operator-hub"})
+
+        if start_sbo == "local":
+            os.getenv("TEST_ACCEPTANCE_SBO_STARTED") | should_not.start_with("FAILED").desc("TEST_ACCEPTANCE_SBO_STARTED is not FAILED")
+        elif start_sbo == "operator-hub":
+            return False
+
+        return self.check_resources()
diff --git a/test/acceptance/features/steps/steps.py b/test/acceptance/features/steps/steps.py
new file mode 100644
index 00000000..ec881eb2
--- /dev/null
+++ b/test/acceptance/features/steps/steps.py
@@ -0,0 +1,188 @@
+# @mark.steps
+# ----------------------------------------------------------------------------
+# STEPS:
+# ----------------------------------------------------------------------------
+import os
+import re
+
+from behave import given, then, when
+from pyshould import should, should_not
+
+from servicebindingoperator import Servicebindingoperator
+from dboperator import DbOperator
+from openshift import Openshift
+from postgres_db import PostgresDB
+from namespace import Namespace
+from nodejs_application import NodeJSApp
+from service_binding_request import ServiceBindingRequest
+
+
+# STEP
+@given(u'Namespace "{namespace_name}" is used')
+def given_namespace_is_used(context, namespace_name):
+    namespace = Namespace(namespace_name)
+    if not namespace.is_present():
+        print("Namespace is not present, creating namespace: {}...".format(namespace_name))
+        namespace.create() | should.be_truthy.desc("Namespace {} is created".format(namespace_name))
+    print("Namespace {} is created!!!".format(namespace_name))
+    context.namespace = namespace
+
+
+# STEP
+@given(u'Namespace [{namespace_env}] is used')
+def given_namespace_from_env_is_used(context, namespace_env):
+    env = os.getenv(namespace_env, "")
+    env | should_not.be_none.desc(f"{namespace_env} env variable is set")
+    print(f"{namespace_env} = {env}")
+    given_namespace_is_used(context, env)
+
+
+# STEP
+sbo_is_running_in_namespace_step = u'Service Binding Operator is running in "{operator_namespace}" namespace'
+
+
+@given(sbo_is_running_in_namespace_step)
+@when(sbo_is_running_in_namespace_step)
+def sbo_is_running_in_namespace(context, operator_namespace):
+    """
+    Checks if the SBO is up and running in the given namespace
+    """
+    sb_operator = Servicebindingoperator(namespace=operator_namespace)
+    sb_operator.is_running() | should.be_truthy.desc("Service Binding Operator is running")
+    print("Service binding operator is running!!!")
+
+
+# STEP
+sbo_is_running_in_namespace_from_env_step = u'Service Binding Operator is running in [{operator_namespace_env}] namespace'
+
+
+@given(sbo_is_running_in_namespace_from_env_step)
+@when(sbo_is_running_in_namespace_from_env_step)
+def sbo_is_running_in_namespace_from_env(context, operator_namespace_env):
+    env = os.getenv(operator_namespace_env, "")
+    env | should_not.be_none.desc(f"{operator_namespace_env} env variable is set")
+    print(f"{operator_namespace_env} = {env}")
+    sbo_is_running_in_namespace(context, env)
+
+
+# STEP
+sbo_is_running_step = u'Service Binding Operator is running'
+
+
+@given(sbo_is_running_step)
+@when(sbo_is_running_step)
+def sbo_is_running(context):
+    context.namespace | should_not.be_none.desc("Namespace set in context")
+    sbo_is_running_in_namespace(context, context.namespace.name)
+
+
+# STEP
+@given(u'PostgreSQL DB operator is installed')
+def given_db_operator_is_installed(context):
+    db_operator = DbOperator()
+    if not db_operator.is_running():
+        print("DB operator is not installed, installing...")
+        db_operator.install_operator_source() | should.be_truthy.desc("DB operator source installed")
+        db_operator.install_operator_subscription() | should.be_truthy.desc("DB operator subscription installed")
+        db_operator.is_running(wait=True) | should.be_truthy.desc("DB operator installed")
+    print("PostgresSQL DB operator is running!!!")
+
+
+# STEP
+imported_nodejs_app_is_running_step = u'Imported Nodejs application "{application_name}" is running'
+
+
+@given(imported_nodejs_app_is_running_step)
+@when(imported_nodejs_app_is_running_step)
+def imported_nodejs_app_is_running(context, application_name):
+    namespace = context.namespace
+    application = NodeJSApp(application_name, namespace.name)
+    if not application.is_running():
+        print("application is not running, trying to import it")
+        application.install() | should.be_truthy.desc("Application is installed")
+        application.is_running(wait=True) | should.be_truthy.desc("Application is running")
+    print("Nodejs application is running!!!")
+    application.get_db_name_from_api() | should_not.be_none
+    context.nodejs_app_original_generation = application.get_observed_generation()
+    context.nodejs_app_original_pod_name = application.get_running_pod_name()
+    context.nodejs_app = application
+
+
+# STEP
+imported_nodejs_app_is_not_running_step = u'Imported Nodejs application "{application_name}" is not running'
+
+
+@given(imported_nodejs_app_is_not_running_step)
+@when(imported_nodejs_app_is_not_running_step)
+def imported_nodejs_app_is_not_running(context, application_name):
+    namespace = context.namespace
+    application = NodeJSApp(application_name, namespace.name)
+    application.is_running() | should.be_falsy.desc("Aplication not running")
+
+
+# STEP
+db_instance_is_running_step = u'DB "{db_name}" is running'
+
+
+@given(db_instance_is_running_step)
+@when(db_instance_is_running_step)
+def db_instance_is_running(context, db_name):
+    namespace = context.namespace
+
+    db = PostgresDB(db_name, namespace.name)
+    if not db.is_running():
+        db.create() | should.be_truthy.desc("Postgres DB created")
+        db.is_running(wait=True) | should.be_truthy.desc("Postgres DB is running")
+    print(f"DB {db_name} is running!!!")
+
+
+# STEP
+sbr_is_applied_step = u'Service Binding Request is applied to connect the database and the application'
+
+
+@given(sbr_is_applied_step)
+@when(sbr_is_applied_step)
+def sbr_is_applied(context):
+    sbr_yaml = context.text
+    sbr = ServiceBindingRequest()
+    if context.__contains__("nodejs_app"):
+        application = context.nodejs_app
+        context.nodejs_app_original_generation = application.get_observed_generation()
+        context.nodejs_app_original_pod_name = application.get_running_pod_name()
+    sbr.create(sbr_yaml) | should.be_truthy.desc("Service Binding Request Created")
+
+
+# STEP
+@then(u'application should be re-deployed')
+def then_application_redeployed(context):
+    application = context.nodejs_app
+    application.get_redeployed_pod_name(context.nodejs_app_original_pod_name) | should_not.be_none.desc(
+        "There is a running pod of the application different from the original one before redeployment.")
+
+
+# STEP
+@then(u'application should be connected to the DB "{db_name}"')
+def then_app_is_connected_to_db(context, db_name):
+    application = context.nodejs_app
+    app_db_name = application.get_db_name_from_api()
+    app_db_name | should.be_equal_to(db_name)
+
+
+# STEP
+@then(u'jsonpath "{json_path}" of Service Binding Request "{sbr_name}" should be changed to "{json_value_regex}"')
+def then_sbo_jsonpath_is(context, json_path, sbr_name, json_value_regex):
+    openshift = Openshift()
+    openshift.search_resource_in_namespace("servicebindingrequests", sbr_name, context.namespace.name) | should_not.be_none.desc("SBR {sbr_name} exists")
+    result = openshift.get_resource_info_by_jsonpath("sbr", sbr_name, context.namespace.name, json_path, wait=True)
+    result | should_not.be_none.desc("jsonpath result")
+    re.fullmatch(json_value_regex, result) | should_not.be_none.desc("SBO jsonpath result \"{result}\" should match \"{json_value_regex}\"")
+
+
+# STEP
+@then(u'jq "{jq_expression}" of Service Binding Request "{sbr_name}" should be changed to "{json_value_regex}"')
+def then_sbo_jq_is(context, jq_expression, sbr_name, json_value_regex):
+    openshift = Openshift()
+    openshift.search_resource_in_namespace("servicebindingrequests", sbr_name, context.namespace.name) | should_not.be_none.desc("SBR {sbr_name} exists")
+    result = openshift.get_resource_info_by_jq("sbr", sbr_name, context.namespace.name, jq_expression, wait=True)
+    result | should_not.be_none.desc("jq result")
+    re.fullmatch(json_value_regex, result) | should_not.be_none.desc("SBO jq result \"{result}\" should match \"{json_value_regex}\"")
diff --git a/test/e2e/servicebindingrequest_test.go b/test/e2e/servicebindingrequest_test.go
index d88a6715..9d75ccdf 100644
--- a/test/e2e/servicebindingrequest_test.go
+++ b/test/e2e/servicebindingrequest_test.go
@@ -498,10 +498,10 @@ func serviceBindingRequestTest(
 	// operator reconciliation.
 	t.Log("Inspecting deployment structure...")
 	require.Eventually(t, func() bool {
-		t.Logf("Inspecting deployment '%s'", deploymentNamespacedName)
+		t.Logf("Inspecting deployment: '%s'", deploymentNamespacedName)
 		_, err := assertDeploymentEnvFrom(todoCtx, f, deploymentNamespacedName, sbrName)
 		if err != nil {
-			t.Logf("Error on inspecting deployment: '%s'", err)
+			t.Logf("Error on inspecting deployment: '%#v'", err)
 			return false
 		}
 		t.Logf("Deployment: Result after attempts, error: '%#v'", err)
@@ -521,26 +521,31 @@ func serviceBindingRequestTest(
 		return true
 	}, 120*time.Second, 2*time.Second)
 
+	// checking intermediary secret contents, right after deployment the secrets must be in place
+	t.Log("Checking intermediary secret contents...")
 	require.Eventually(t, func() bool {
-		// checking intermediary secret contents, right after deployment the secrets must be in place
+		t.Logf("Inspecting SBR secret: '%s'", intermediarySecretNamespacedName)
 		sbrSecret, err := assertSBRSecret(todoCtx, f, intermediarySecretNamespacedName, assertKeys)
 		if err != nil {
 			// it can be that assertSBRSecret returns nil until service configuration values can be
 			// collected.
-			t.Logf("binding secret contents are invalid: %v", sbrSecret)
+			t.Logf("Binding secret contents are invalid: '%#v'", sbrSecret)
 			return false
 		}
+		t.Logf("SBR-Secret: Result after attempts, error: '%#v'", err)
 		return true
 	}, 120*time.Second, 2*time.Second)
 
 	// editing intermediary secret in order to trigger update event
+	t.Log("Editing intermediary secret in order to trigger update event...")
 	require.Eventually(t, func() bool {
-		t.Logf("Updating intermediary secret to have bogus data: '%s'", intermediarySecretNamespacedName)
+		t.Logf("Updating intermediary secret: '%s'", intermediarySecretNamespacedName)
 		err := updateSBRSecret(todoCtx, t, f, intermediarySecretNamespacedName)
 		if err != nil {
-			t.Log(err)
+			t.Logf("Error on updating intermediary secrets: '%#v'", err)
 			return false
 		}
+		t.Logf("SBR-Secret: Result after update, error: '%#v'", err)
 		return true
 	}, 120*time.Second, 2*time.Second)
 
@@ -548,7 +553,7 @@ func serviceBindingRequestTest(
 	// reconcile again when detecting the change
 	t.Log("Inspecting intermediary secret...")
 	require.Eventually(t, func() bool {
-		t.Logf("Inspecting secret '%s'...", intermediarySecretNamespacedName)
+		t.Logf("Inspecting secret: '%s'", intermediarySecretNamespacedName)
 		_, err := assertSBRSecret(todoCtx, f, intermediarySecretNamespacedName, assertKeys)
 		if err != nil {
 			t.Logf("Secret inspection error: '%#v'", err)
@@ -559,12 +564,14 @@ func serviceBindingRequestTest(
 	}, 120*time.Second, 2*time.Second)
 
 	// executing deletion of the request, triggering unbinding actions
+	t.Log("Executing deletion of the request, triggering unbinding actions...")
 	err := f.Client.Delete(todoCtx, sbr)
 	require.NoError(t, err, "expect deletion to not return errors")
 
 	// after deletion, secret should not be found anymore
+	t.Log("Looking for intermediary secrets after deleting sbr...")
 	require.Eventually(t, func() bool {
-		t.Logf("Searching for secret '%s'...", intermediarySecretNamespacedName)
+		t.Logf("Searching for secret: '%s'", intermediarySecretNamespacedName)
 		err := assertSecretNotFound(todoCtx, f, intermediarySecretNamespacedName)
 		if err != nil {
 			t.Logf("Secret search error: '%#v'", err)
@@ -575,13 +582,16 @@ func serviceBindingRequestTest(
 	}, 120*time.Second, 2*time.Second)
 
 	// after deletion, deployment should not contain envFrom directive anymore
+	t.Log("Looking for envFrom directive in deployment after deleting sbr...")
 	require.Eventually(t, func() bool {
-		t.Logf("Inspecting deployment '%s'", deploymentNamespacedName)
+		t.Log("Expect deployment not to be carrying envFrom directive")
+		t.Logf("Inspecting deployment: '%s'", deploymentNamespacedName)
 		_, err := assertDeploymentEnvFrom(todoCtx, f, deploymentNamespacedName, sbrName)
 		if err != nil {
-			t.Log("expect deployment not to be carrying envFrom directive")
+			t.Logf("Did not find envFrom directive, error: '%#v'", err)
 			return true
 		}
+		t.Logf("Deployment contains envFrom directive, error: '%#v'", err)
 		return false
 	}, 120*time.Second, 2*time.Second)
 }
diff --git a/test/mocks/fake.go b/test/mocks/fake.go
index d03fd75a..f743195f 100644
--- a/test/mocks/fake.go
+++ b/test/mocks/fake.go
@@ -187,6 +187,14 @@ func (f *Fake) AddMockedUnstructuredSecret(name string) *unstructured.Unstructur
 	return s
 }
 
+// AddMockedUnstructuredSecret add mocked object from SecretMock. This secret is created with a resourceVersion
+func (f *Fake) AddMockedUnstructuredSecretRV(name string) *unstructured.Unstructured {
+	s, err := UnstructuredSecretMockRV(f.ns, name)
+	require.NoError(f.t, err)
+	f.objs = append(f.objs, s)
+	return s
+}
+
 // AddNamespacedMockedSecret add mocked object from SecretMock in a namespace
 // which isn't necessarily same as that of the ServiceBindingRequest namespace.
 func (f *Fake) AddNamespacedMockedSecret(name string, namespace string, data map[string][]byte) {
diff --git a/test/mocks/mocks.go b/test/mocks/mocks.go
index baf58ffa..febc3f26 100644
--- a/test/mocks/mocks.go
+++ b/test/mocks/mocks.go
@@ -164,6 +164,11 @@ func UnstructuredSecretMock(ns, name string) (*unstructured.Unstructured, error)
 	return converter.ToUnstructured(&s)
 }
 
+func UnstructuredSecretMockRV(ns, name string) (*unstructured.Unstructured, error) {
+	s := SecretMockRV(ns, name)
+	return converter.ToUnstructured(&s)
+}
+
 func UnstructuredPostgresDatabaseCRMock(ns, name string) (*unstructured.Unstructured, error) {
 	c := PostgresDatabaseCRMock(ns, name)
 	return converter.ToUnstructured(&c)
@@ -374,6 +379,25 @@ func SecretMock(ns, name string, data map[string][]byte) *corev1.Secret {
 	}
 }
 
+// SecretMockRV returns a Secret with a resourceVersion.
+func SecretMockRV(ns, name string) *corev1.Secret {
+	return &corev1.Secret{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Secret",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace:       ns,
+			Name:            name,
+			ResourceVersion: "116076",
+		},
+		Data: map[string][]byte{
+			"user":     []byte("user"),
+			"password": []byte("password"),
+		},
+	}
+}
+
 // ConfigMapMock returns a dummy config-map object.
 func ConfigMapMock(ns, name string) *corev1.ConfigMap {
 	return &corev1.ConfigMap{