Repository: redhat-developer/service-binding-operator
Pull request #551 (2020-07-10 21:17:19)

Delete sbr if it's ownerRef resource is deleted
### Motivation

Fix issue #542 
dependent on #523 , #498 

### Testing

Please follow steps https://github.com/redhat-developer/service-binding-operator/issues/542#issue-652029225 manually
need to add an e2e test case since garbage collection is involved here. e2e can be taken up after the acceptance tests are in place. 

For further more details refer the CONTRIBUTING.md

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 128b04a7..84be7a76 100644
--- a/Makefile
+++ b/Makefile
@@ -45,6 +45,8 @@ endif
 
 ZAP_FLAGS = $(ZAP_ENCODER_FLAG) $(ZAP_LEVEL_FLAG)
 
+SKIP_CLEANUP_ERROR ?= true
+
 # Create output directory for artifacts and test results. ./out is supposed to
 # be a safe place for all targets to write to while knowing that all content
 # inside of ./out is wiped once "make clean" is run.
@@ -154,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
@@ -177,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:
@@ -219,7 +229,7 @@ test-e2e: e2e-setup deploy-crds
 		operator-sdk --verbose test local ./test/e2e \
 			--namespace $(TEST_NAMESPACE) \
 			--up-local \
-			--skip-cleanup-error=true \
+			--skip-cleanup-error=$(SKIP_CLEANUP_ERROR) \
 			--go-test-flags "-timeout=110m" \
 			--local-operator-flags "$(ZAP_FLAGS)" \
 			$(OPERATOR_SDK_EXTRA_ARGS) \
@@ -265,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
@@ -479,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/annotations/annotations.go b/pkg/controller/servicebindingrequest/annotations/annotations.go
index f4c4ace4..b19e17b0 100644
--- a/pkg/controller/servicebindingrequest/annotations/annotations.go
+++ b/pkg/controller/servicebindingrequest/annotations/annotations.go
@@ -40,6 +40,11 @@ type result struct {
 	Type bindingType
 	// Path is the nested location the collected data can be found in the Data field.
 	Path string
+	// RawData contains the annotation data collected by an annotation handler
+	// inside a deep structure with its root being composed by the path where
+	// the external resource name was extracted and the path within the external
+	// resource.
+	RawData map[string]interface{}
 }
 
 // handler should be implemented by types that want to offer a mechanism to provide binding data to
diff --git a/pkg/controller/servicebindingrequest/annotations/resource.go b/pkg/controller/servicebindingrequest/annotations/resource.go
index 40dafe9d..ec309570 100644
--- a/pkg/controller/servicebindingrequest/annotations/resource.go
+++ b/pkg/controller/servicebindingrequest/annotations/resource.go
@@ -149,10 +149,17 @@ func (h *resourceHandler) Handle() (result, error) {
 	// prefix the output path with the kind of the resource.
 	outputPath := strings.Join(outputPathParts, ".")
 
+	rawDataPath := strings.Join([]string{
+		h.bindingInfo.ResourceReferencePath,
+		h.bindingInfo.SourcePath,
+	}, ".")
+
 	return result{
 		Data: nested.ComposeValue(val, nested.NewPath(outputPath)),
 		Type: typ,
 		Path: outputPath,
+
+		RawData: nested.ComposeValue(val, nested.NewPath(rawDataPath)),
 	}, nil
 }
 
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/reconciler.go b/pkg/controller/servicebindingrequest/reconciler.go
index aaab75b8..004fdf7b 100644
--- a/pkg/controller/servicebindingrequest/reconciler.go
+++ b/pkg/controller/servicebindingrequest/reconciler.go
@@ -3,6 +3,7 @@ package servicebindingrequest
 import (
 	"context"
 	"errors"
+	"sort"
 
 	conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -75,6 +76,21 @@ func (r *reconciler) getServiceBindingRequest(
 	return sbr, nil
 }
 
+// distinctServiceSelectors returns the distinct elements from the given service selector slice.
+func distinctServiceSelectors(selectors []v1alpha1.BackingServiceSelector) []v1alpha1.BackingServiceSelector {
+	distinct := make(map[v1alpha1.BackingServiceSelector]bool)
+	for _, v := range selectors {
+		distinct[v] = true
+	}
+
+	var output []v1alpha1.BackingServiceSelector
+	for k := range distinct {
+		output = append(output, k)
+	}
+
+	return output
+}
+
 // extractServiceSelectors returns a list of all BackingServiceSelector items from a
 // ServiceBindingRequest.
 //
@@ -92,7 +108,17 @@ func extractServiceSelectors(
 	if inSelectors != nil {
 		selectors = append(selectors, *inSelectors...)
 	}
-	return selectors
+
+	// FIXME(isuttonl): sorting selectors using name and namespace can be more robust.
+	sort.Slice(selectors, func(i, j int) bool {
+		a := selectors[i].ResourceRef < selectors[j].ResourceRef
+		b := selectors[j].Namespace != nil &&
+			selectors[i].Namespace != nil &&
+			*selectors[i].Namespace < *selectors[j].Namespace
+		return a && b
+	})
+
+	return distinctServiceSelectors(selectors)
 }
 
 // Reconcile a ServiceBindingRequest by the following steps:
@@ -167,6 +193,7 @@ func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err
 	}
 
 	serviceCtxs, err := buildServiceContexts(
+		logger.WithName("buildServiceContexts"),
 		r.dynClient,
 		sbr.GetNamespace(),
 		selectors,
@@ -217,6 +244,15 @@ func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, err
 		logger.Error(err, "Error add watching application GVR")
 	}
 
+	if sbr.GetDeletionTimestamp() != nil && sbr.GetOwnerReferences() != nil {
+		logger := logger.WithName("Deleting SBR when it has ownerReference")
+		logger.Debug("Removing resource finalizers...")
+		removeFinalizer(sbr)
+		if _, err := updateServiceBindingRequest(r.dynClient, sbr); err != nil {
+			return requeueError(err)
+		}
+		return done()
+	}
 	if sbr.GetDeletionTimestamp() != nil {
 		logger := logger.WithName("unbind")
 		logger.Info("Executing unbinding steps...")
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/pkg/controller/servicebindingrequest/service.go b/pkg/controller/servicebindingrequest/service.go
index a07f0b8e..bc7b2c18 100644
--- a/pkg/controller/servicebindingrequest/service.go
+++ b/pkg/controller/servicebindingrequest/service.go
@@ -196,8 +196,15 @@ func buildOwnedResourceContext(
 	outputPath string,
 ) (*serviceContext, error) {
 	svcCtx, err := buildServiceContext(
-		client, obj.GetNamespace(), obj.GetObjectKind().GroupVersionKind(), obj.GetName(),
-		ownerEnvVarPrefix, restMapper, nil)
+		reconcilerLog.WithName("buildServiceContext"),
+		client,
+		obj.GetNamespace(),
+		obj.GetObjectKind().GroupVersionKind(),
+		obj.GetName(),
+		ownerEnvVarPrefix,
+		restMapper,
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/servicebindingrequest/servicecontext.go b/pkg/controller/servicebindingrequest/servicecontext.go
index 5696a17f..df4fa81e 100644
--- a/pkg/controller/servicebindingrequest/servicecontext.go
+++ b/pkg/controller/servicebindingrequest/servicecontext.go
@@ -11,6 +11,7 @@ import (
 	"k8s.io/client-go/dynamic"
 
 	v1alpha1 "github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1"
+	"github.com/redhat-developer/service-binding-operator/pkg/log"
 )
 
 // serviceContext contains information related to a service.
@@ -49,6 +50,7 @@ func stringValueOrDefault(val *string, defaultVal string) string {
 // buildServiceContexts return a collection of ServiceContext values from the given service
 // selectors.
 func buildServiceContexts(
+	logger *log.Log,
 	client dynamic.Interface,
 	defaultNs string,
 	selectors []v1alpha1.BackingServiceSelector,
@@ -56,12 +58,22 @@ func buildServiceContexts(
 	restMapper meta.RESTMapper,
 ) (serviceContextList, error) {
 	svcCtxs := make(serviceContextList, 0)
+
+SELECTORS:
 	for _, s := range selectors {
 		ns := stringValueOrDefault(s.Namespace, defaultNs)
 		gvk := schema.GroupVersionKind{Kind: s.Kind, Version: s.Version, Group: s.Group}
-		svcCtx, err := buildServiceContext(
-			client, ns, gvk, s.ResourceRef, s.EnvVarPrefix, restMapper, s.Id)
+		svcCtx, err := buildServiceContext(logger.WithName("buildServiceContexts"), client, ns, gvk,
+			s.ResourceRef, s.EnvVarPrefix, restMapper, s.Id)
+
 		if err != nil {
+			// best effort approach; should not break in common cases such as a unknown annotation
+			// prefix (other annotations might exist in the resource) or, in the case of a valid
+			// annotation, the handler expected for the annotation can't be found.
+			if err == annotations.ErrInvalidAnnotationPrefix || annotations.IsErrHandlerNotFound(err) {
+				logger.Trace("Continuing to next selector", "Error", err)
+				continue SELECTORS
+			}
 			return nil, err
 		}
 		svcCtxs = append(svcCtxs, svcCtx)
@@ -119,10 +131,64 @@ func findOwnedResourcesCtxs(
 	)
 }
 
+func merge(dst map[string]interface{}, src map[string]interface{}) (map[string]interface{}, error) {
+	merged := map[string]interface{}{}
+
+	err := mergo.Merge(&merged, src, mergo.WithOverride, mergo.WithOverrideEmptySlice)
+	if err != nil {
+		return nil, err
+	}
+
+	err = mergo.Merge(&merged, dst)
+	if err != nil {
+		return nil, err
+	}
+
+	return merged, nil
+}
+
+func runHandler(
+	client dynamic.Interface,
+	obj *unstructured.Unstructured,
+	outputObj *unstructured.Unstructured,
+	key string,
+	value string,
+	envVars map[string]interface{},
+	volumeKeys *[]string,
+	restMapper meta.RESTMapper,
+) error {
+	h, err := annotations.BuildHandler(client, obj, key, value, restMapper)
+	if err != nil {
+		return err
+	}
+	r, err := h.Handle()
+	if err != nil {
+		return err
+	}
+
+	if newObj, err := merge(outputObj.Object, r.RawData); err != nil {
+		return err
+	} else {
+		outputObj.Object = newObj
+	}
+
+	err = mergo.Merge(&envVars, r.Data, mergo.WithAppendSlice, mergo.WithOverride)
+	if err != nil {
+		return err
+	}
+
+	if r.Type == annotations.BindingTypeVolumeMount {
+		*volumeKeys = []string(append(*volumeKeys, r.Path))
+	}
+
+	return nil
+}
+
 // buildServiceContext inspects g the API server searching for the service resources, associated CRD
 // and OLM's CRDDescription if present, and processes those with relevant annotations to compose a
 // ServiceContext.
 func buildServiceContext(
+	logger *log.Log,
 	client dynamic.Interface,
 	ns string,
 	gvk schema.GroupVersionKind,
@@ -171,37 +237,19 @@ func buildServiceContext(
 	volumeKeys := make([]string, 0)
 	envVars := make(map[string]interface{})
 
-	for annotationKey, annotationValue := range anns {
-		h, err := annotations.BuildHandler(
-			client,
-			obj,
-			annotationKey,
-			annotationValue,
-			restMapper,
-		)
-		if err != nil {
-			if err == annotations.ErrInvalidAnnotationPrefix || annotations.IsErrHandlerNotFound(err) {
-				continue
-			}
-			return nil, err
-		}
-		r, err := h.Handle()
-		if err != nil {
-			continue
-		}
+	// outputObj will be used to keep the changes processed by the handler.
+	outputObj := obj.DeepCopy()
 
-		err = mergo.Merge(&envVars, r.Data, mergo.WithAppendSlice, mergo.WithOverride)
+	for k, v := range anns {
+		// runHandler modifies 'outputObj', 'envVars' and 'volumeKeys' in place.
+		err := runHandler(client, obj, outputObj, k, v, envVars, &volumeKeys, restMapper)
 		if err != nil {
-			return nil, err
-		}
-
-		if r.Type == annotations.BindingTypeVolumeMount {
-			volumeKeys = append(volumeKeys, r.Path)
+			logger.Debug("Failed executing runHandler", "Error", err)
 		}
 	}
 
 	serviceCtx := &serviceContext{
-		service:      obj,
+		service:      outputObj,
 		envVars:      envVars,
 		volumeKeys:   volumeKeys,
 		envVarPrefix: envVarPrefix,
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/retriever_test.go b/pkg/controller/servicebindingrequest/retriever_test.go
index 38ecb146..41bc3cad 100644
--- a/pkg/controller/servicebindingrequest/retriever_test.go
+++ b/pkg/controller/servicebindingrequest/retriever_test.go
@@ -37,7 +37,7 @@ func TestRetrieverProcessServiceContexts(t *testing.T) {
 
 	f := mocks.NewFake(t, ns)
 	f.AddMockedUnstructuredCSV("csv")
-	f.AddNamespacedMockedSecret("db-credentials", backingServiceNs)
+	f.AddNamespacedMockedSecret("db-credentials", backingServiceNs, nil)
 
 	cr, err := mocks.UnstructuredDatabaseCRMock(backingServiceNs, crName)
 	require.NoError(t, err)
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/pkg/controller/servicebindingrequest/service_test.go b/pkg/controller/servicebindingrequest/service_test.go
index a2b9d951..eedf5895 100644
--- a/pkg/controller/servicebindingrequest/service_test.go
+++ b/pkg/controller/servicebindingrequest/service_test.go
@@ -50,7 +50,7 @@ func TestPlannerWithExplicitBackingServiceNamespace(t *testing.T) {
 	f.AddMockedUnstructuredDatabaseCRD()
 	f.AddMockedUnstructuredCSV("cluster-service-version")
 	db := f.AddMockedDatabaseCR(resourceRef, backingServiceNamespace)
-	f.AddNamespacedMockedSecret("db-credentials", backingServiceNamespace)
+	f.AddNamespacedMockedSecret("db-credentials", backingServiceNamespace, nil)
 
 	t.Run("findService", func(t *testing.T) {
 		cr, err := findService(
diff --git a/pkg/controller/servicebindingrequest/servicecontext_test.go b/pkg/controller/servicebindingrequest/servicecontext_test.go
index cfd323eb..cb9324ac 100644
--- a/pkg/controller/servicebindingrequest/servicecontext_test.go
+++ b/pkg/controller/servicebindingrequest/servicecontext_test.go
@@ -6,6 +6,7 @@ import (
 	routev1 "github.com/openshift/api/route/v1"
 	pgv1alpha1 "github.com/operator-backing-service-samples/postgresql-operator/pkg/apis/postgresql/v1alpha1"
 	"github.com/redhat-developer/service-binding-operator/pkg/apis/apps/v1alpha1"
+	"github.com/redhat-developer/service-binding-operator/pkg/log"
 	"github.com/redhat-developer/service-binding-operator/pkg/testutils"
 	"github.com/redhat-developer/service-binding-operator/test/mocks"
 	"github.com/stretchr/testify/require"
@@ -15,43 +16,130 @@ import (
 )
 
 func TestBuildServiceContexts(t *testing.T) {
-	ns := "planner"
-	name := "service-binding-request"
-	resourceRef := "db-testing"
-	matchLabels := map[string]string{
-		"connects-to": "database",
-		"environment": "planner",
-	}
-	f := mocks.NewFake(t, ns)
-	sbr := f.AddMockedServiceBindingRequest(name, nil, resourceRef, "", deploymentsGVR, matchLabels)
-	sbr.Spec.BackingServiceSelectors = &[]v1alpha1.BackingServiceSelector{
-		*sbr.Spec.BackingServiceSelector,
-	}
-	f.AddMockedUnstructuredCSV("cluster-service-version")
-	f.AddMockedDatabaseCR(resourceRef, ns)
-	f.AddMockedUnstructuredDatabaseCRD()
-	f.AddMockedUnstructuredSecret("db-credentials")
-
+	logger := log.NewLog("testBuildServiceContexts")
 	restMapper := testutils.BuildTestRESTMapper()
 
-	t.Run("existing selectors", func(t *testing.T) {
+	t.Run("empty selectors", func(t *testing.T) {
+		ns := "planner"
+		f := mocks.NewFake(t, ns)
 		serviceCtxs, err := buildServiceContexts(
-			f.FakeDynClient(), ns, extractServiceSelectors(sbr), false, restMapper)
-		require.NoError(t, err)
-		require.NotEmpty(t, serviceCtxs)
+			logger, f.FakeDynClient(), ns, nil, false, restMapper)
+
+		require.NoError(t, err, "buildServiceContexts must execute without errors")
+		require.Empty(t, serviceCtxs, "buildServiceContexts must be empty")
 	})
 
-	t.Run("empty selectors", func(t *testing.T) {
-		serviceCtxs, err := buildServiceContexts(f.FakeDynClient(), ns, nil, false, restMapper)
-		require.NoError(t, err)
-		require.Empty(t, serviceCtxs)
+	t.Run("declared and existing selectors", func(t *testing.T) {
+		sbrName := "service-binding-request"
+		firstResourceRef := "db-testing"
+		firstNamespace := "existing-namespace"
+		matchLabels := map[string]string{
+			"connects-to": "database",
+			"environment": "planner",
+		}
+
+		f := mocks.NewFake(t, firstNamespace)
+		f.AddMockedUnstructuredCSV("cluster-service-version")
+		f.AddMockedDatabaseCR(firstResourceRef, firstNamespace)
+		f.AddMockedUnstructuredDatabaseCRD()
+		f.AddNamespacedMockedSecret("db-credentials", firstNamespace, nil)
+
+		sbr := f.AddMockedServiceBindingRequest(sbrName, nil, firstResourceRef, "", deploymentsGVR, matchLabels)
+
+		serviceCtxs, err := buildServiceContexts(
+			logger, f.FakeDynClient(), firstNamespace, extractServiceSelectors(sbr), false, restMapper)
+
+		require.NoError(t, err, "buildServiceContexts must execute without errors")
+		require.Len(t, serviceCtxs, 1, "buildServiceContexts must return only one item")
+
+		serviceCtx := serviceCtxs[0]
+		expectedKeys := []string{"status", "dbCredentials"}
+		expectedDbCredentials := map[string]interface{}{
+			"username": "user",
+			"password": "password",
+		}
+
+		gotDbCredentials, ok, err :=
+			unstructured.NestedFieldCopy(serviceCtx.service.Object, expectedKeys...)
+		require.NoError(t, err, "must not return error while copying status.dbCredentials out of the context's service")
+		require.True(t, ok, "status.dbCredentials must exist")
+		require.Equal(t, expectedDbCredentials, gotDbCredentials, "status.dbCredentials in context must be equal to expected")
 	})
 
 	t.Run("services in different namespace", func(t *testing.T) {
+		sameNs := "same-ns"
+		sameNsResourceRef := "same-ns-database"
+
+		otherNs := "other-ns"
+		otherNsResourceRef := "other-ns-database"
+
+		matchLabels := map[string]string{
+			"connects-to": "database",
+			"environment": "planner",
+		}
+
+		f := mocks.NewFake(t, sameNs)
+		f.AddMockedUnstructuredDatabaseCRD()
+		f.AddMockedDatabaseCR(sameNsResourceRef, sameNs)
+		f.AddNamespacedMockedSecret("db-credentials", sameNs, map[string][]byte{
+			"username": []byte("same-ns-username"),
+			"password": []byte("same-ns-password"),
+		})
+
+		f.AddMockedDatabaseCR(otherNsResourceRef, otherNs)
+		f.AddNamespacedMockedSecret("db-credentials", otherNs, map[string][]byte{
+			"username": []byte("other-ns-username"),
+			"password": []byte("other-ns-password"),
+		})
+
+		sbrName := "services-in-different-ns"
+
+		sbr := f.AddMockedServiceBindingRequest(sbrName, &sameNs, sameNsResourceRef, "", deploymentsGVR, matchLabels)
+		sbr.Spec.BackingServiceSelectors = &[]v1alpha1.BackingServiceSelector{
+			{
+				GroupVersionKind: metav1.GroupVersionKind{
+					Group:   mocks.CRDName,
+					Version: mocks.CRDVersion,
+					Kind:    mocks.CRDKind,
+				},
+				ResourceRef: otherNsResourceRef,
+				Namespace:   &otherNs,
+			},
+		}
+
 		serviceCtxs, err := buildServiceContexts(
-			f.FakeDynClient(), ns, extractServiceSelectors(sbr), false, restMapper)
-		require.NoError(t, err)
-		require.NotEmpty(t, serviceCtxs)
+			logger, f.FakeDynClient(), sameNs, extractServiceSelectors(sbr), false, restMapper)
+
+		require.NoError(t, err, "buildServiceContexts must execute without errors")
+		require.Len(t, serviceCtxs, 2, "buildServiceContexts must return both service contexts")
+
+		{
+			otherNsCtx := serviceCtxs[0]
+			expectedKeys := []string{"status", "dbCredentials"}
+			expectedDbCredentials := map[string]interface{}{
+				"username": "other-ns-username",
+				"password": "other-ns-password",
+			}
+			gotDbCredentials, ok, err :=
+				unstructured.NestedFieldCopy(otherNsCtx.service.Object, expectedKeys...)
+			require.NoError(t, err)
+			require.True(t, ok)
+			require.Equal(t, expectedDbCredentials, gotDbCredentials)
+		}
+
+		{
+			sameNsCtx := serviceCtxs[1]
+			expectedKeys := []string{"status", "dbCredentials"}
+			expectedDbCredentials := map[string]interface{}{
+				"username": "same-ns-username",
+				"password": "same-ns-password",
+			}
+			gotDbCredentials, ok, err :=
+				unstructured.NestedFieldCopy(sameNsCtx.service.Object, expectedKeys...)
+			require.NoError(t, err)
+			require.True(t, ok)
+			require.Equal(t, expectedDbCredentials, gotDbCredentials)
+		}
 	})
 }
 
@@ -120,7 +208,7 @@ func TestFindOwnedResourcesCtxs_ConfigMap(t *testing.T) {
 		expected := map[string]interface{}{
 			"": map[string]interface{}{
 				"password": "password",
-				"user":     "user",
+				"username": "user",
 			},
 		}
 		require.Equal(t, expected, got[0].envVars)
@@ -167,7 +255,7 @@ func TestFindOwnedResourcesCtxs_Secrets(t *testing.T) {
 			restMapper := testutils.BuildTestRESTMapper()
 
 			for _, secret := range tC.secrets {
-				secret := mocks.SecretMock("test", secret)
+				secret := mocks.SecretMock("test", secret, nil)
 				us := &unstructured.Unstructured{}
 				uc, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&secret)
 				require.NoError(t, err)
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 8656a71a..9d75ccdf 100644
--- a/test/e2e/servicebindingrequest_test.go
+++ b/test/e2e/servicebindingrequest_test.go
@@ -51,7 +51,7 @@ var (
 	// Intermediate secret should have following data
 	// for postgres operator
 	postgresSecretAssertion = map[string]string{
-		"DATABASE_SECRET_USER":     "user",
+		"DATABASE_SECRET_USERNAME": "user",
 		"DATABASE_SECRET_PASSWORD": "password",
 	}
 
@@ -303,6 +303,14 @@ func CreateDB(
 	ns := namespacedName.Namespace
 	resourceRef := namespacedName.Name
 
+	// order is important: the operator will follow first the database resource,
+	// then the secret holding the credential; in the case the secret is not
+	// there, default values will be set for each of the contributed
+	// configuration values declared in the Database custom resource definition.
+	t.Log("Creating Database credentials secret mock object...")
+	dbSecret := mocks.SecretMock(ns, secretName, nil)
+	require.NoError(t, f.Client.Create(ctx, dbSecret, cleanupOpts))
+
 	db := mocks.DatabaseCRMock(ns, resourceRef)
 	require.NoError(t, f.Client.Create(ctx, db, cleanupOpts))
 
@@ -320,10 +328,6 @@ func CreateDB(
 		return true
 	}, 10*time.Second, 1*time.Second)
 
-	t.Log("Creating Database credentials secret mock object...")
-	dbSecret := mocks.SecretMock(ns, secretName)
-	require.NoError(t, f.Client.Create(ctx, dbSecret, cleanupOpts))
-
 	return db
 }
 
@@ -494,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)
@@ -517,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)
 
@@ -544,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)
@@ -555,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)
@@ -571,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 85a5de1f..f743195f 100644
--- a/test/mocks/fake.go
+++ b/test/mocks/fake.go
@@ -187,10 +187,18 @@ 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) {
-	f.objs = append(f.objs, SecretMock(namespace, name))
+func (f *Fake) AddNamespacedMockedSecret(name string, namespace string, data map[string][]byte) {
+	f.objs = append(f.objs, SecretMock(namespace, name, data))
 }
 
 // AddMockedUnstructuredConfigMap add mocked object from ConfigMapMock.
diff --git a/test/mocks/mocks.go b/test/mocks/mocks.go
index 210187c3..febc3f26 100644
--- a/test/mocks/mocks.go
+++ b/test/mocks/mocks.go
@@ -98,7 +98,7 @@ func DatabaseCRDMock(ns string) apiextensionv1beta1.CustomResourceDefinition {
 	FullCRDName := CRDPlural + "." + CRDName
 	annotations := map[string]string{
 		"servicebindingoperator.redhat.io/status.dbCredentials-password": "binding:env:object:secret",
-		"servicebindingoperator.redhat.io/status.dbCredentials-user":     "binding:env:object:secret",
+		"servicebindingoperator.redhat.io/status.dbCredentials-username": "binding:env:object:secret",
 	}
 
 	crd := apiextensionv1beta1.CustomResourceDefinition{
@@ -160,7 +160,12 @@ func PostgresDatabaseCRMock(ns, name string) PostgresDatabase {
 }
 
 func UnstructuredSecretMock(ns, name string) (*unstructured.Unstructured, error) {
-	s := SecretMock(ns, name)
+	s := SecretMock(ns, name, nil)
+	return converter.ToUnstructured(&s)
+}
+
+func UnstructuredSecretMockRV(ns, name string) (*unstructured.Unstructured, error) {
+	s := SecretMockRV(ns, name)
 	return converter.ToUnstructured(&s)
 }
 
@@ -353,7 +358,14 @@ func UnstructuredDatabaseCRMock(ns, name string) (*unstructured.Unstructured, er
 }
 
 // SecretMock returns a Secret based on PostgreSQL operator usage.
-func SecretMock(ns, name string) *corev1.Secret {
+func SecretMock(ns, name string, data map[string][]byte) *corev1.Secret {
+	if data == nil {
+		data = map[string][]byte{
+			"username": []byte("user"),
+			"password": []byte("password"),
+		}
+	}
+
 	return &corev1.Secret{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Secret",
@@ -363,6 +375,22 @@ func SecretMock(ns, name string) *corev1.Secret {
 			Namespace: ns,
 			Name:      name,
 		},
+		Data: data,
+	}
+}
+
+// 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"),
@@ -382,7 +410,7 @@ func ConfigMapMock(ns, name string) *corev1.ConfigMap {
 			Name:      name,
 		},
 		Data: map[string]string{
-			"user":     "user",
+			"username": "user",
 			"password": "password",
 		},
 	}
diff --git a/test/third-party-crds/postgresql_v1alpha1_database_crd.yaml b/test/third-party-crds/postgresql_v1alpha1_database_crd.yaml
index a0c4beda..09452ec6 100644
--- a/test/third-party-crds/postgresql_v1alpha1_database_crd.yaml
+++ b/test/third-party-crds/postgresql_v1alpha1_database_crd.yaml
@@ -12,7 +12,7 @@ metadata:
     servicebindingoperator.redhat.io/status.dbConnectionIP: binding:env:attribute
     servicebindingoperator.redhat.io/status.dbConnectionPort: binding:env:attribute
     servicebindingoperator.redhat.io/status.dbCredentials-password: binding:env:object:secret
-    servicebindingoperator.redhat.io/status.dbCredentials-user: binding:env:object:secret
+    servicebindingoperator.redhat.io/status.dbCredentials-username: binding:env:object:secret
     servicebindingoperator.redhat.io/status.dbName: binding:env:attribute
 spec:
   group: postgresql.baiju.dev