Repository: Azure/ARO-RP
Pull request #2062 (2022-04-19 15:19:21)

Additional gateway tests
### Which issue this PR addresses:

[ADO WI](https://msazure.visualstudio.com/AzureRedHatOpenShift/_boards/board/t/Azure%20Red%20Hat%20OpenShift/Stories/?workitem=13703173)

### What this PR does / why we need it:

Increases coverage for pkg/gateway

### Test plan for issue:
n/a

### Is there any documentation that needs to be updated for this PR?
n/a


github-actions[bot]: Please rebase pull request.
s-amann: /azp run e2e
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
s-amann: /azp run e2e
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
s-amann: /azp run e2e
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>

Patch:
diff --git a/.github/workflows/golint.yml b/.github/workflows/golint.yml
new file mode 100644
index 000000000..c73e98cce
--- /dev/null
+++ b/.github/workflows/golint.yml
@@ -0,0 +1,49 @@
+name: golangci-lint
+on:
+  push:
+    tags:
+      - v*
+    branches:
+      - master
+      - main
+  pull_request:
+permissions:
+  contents: read
+  # Optional: allow read access to pull request. Use with `only-new-issues` option.
+  # pull-requests: read
+jobs:
+  golangci:
+    name: lint
+    runs-on: ubuntu-latest
+    steps:
+      - run: | 
+          sudo apt-get update 
+          sudo apt-get install libgpgme-dev libgpgme11
+      - uses: actions/setup-go@v3
+        with:
+          go-version: 1.17
+      - uses: actions/checkout@v3
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
+          version: v1.45.2
+
+          # Optional: working directory, useful for monorepos
+          #working-directory: pkg
+
+          # Optional: golangci-lint command line arguments.
+          #args: -c .golangci.yml
+
+          # Optional: show only new issues if it's a pull request. The default value is `false`.
+          # only-new-issues: true
+
+          # Optional: if set to true then the all caching functionality will be complete disabled,
+          #           takes precedence over all other caching options.
+          # skip-cache: true
+
+          # Optional: if set to true then the action don't cache or restore ~/go/pkg.
+          # skip-pkg-cache: true
+
+          # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
+          # skip-build-cache: true
diff --git a/.golangci.yml b/.golangci.yml
index 682ae7a6d..8c8b3f88d 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -16,7 +16,10 @@ linters-settings:
   stylecheck:
     # added additional checks for comments in Go.
     # Refer https://staticcheck.io/docs/options#checks for details
-    checks: ["all", "-ST1000", "ST1020", "ST1021", "ST1022"]
+    checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
+    dot-import-whitelist:
+      - github.com/onsi/ginkgo
+      - github.com/onsi/gomega
 
 
 linters:
@@ -28,6 +31,5 @@ linters:
     - gosimple
     - govet
     - staticcheck
-    # TODO: Enable the stylecheck linter in a follow-up PR as it requires changes in a lot of files
-    # - stylecheck
     - whitespace
+    - stylecheck
diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml
index 6252ba13d..46dce116b 100644
--- a/.pipelines/ci.yml
+++ b/.pipelines/ci.yml
@@ -52,12 +52,6 @@ jobs:
       [[ -z "$(git status -s)" ]]
     displayName: 🕵️ Validate Golang code
 
-  - script: |
-      set -xe
-      make lint-go
-      [[ -z "$(git status -s)" ]]
-    displayName: 🕵️ Lint Golang code
-
   - script: |
       set -xe
       make build-all
diff --git a/docs/prepare-a-shared-rp-development-environment.md b/docs/prepare-a-shared-rp-development-environment.md
index 15a4988ab..3f2a24c23 100644
--- a/docs/prepare-a-shared-rp-development-environment.md
+++ b/docs/prepare-a-shared-rp-development-environment.md
@@ -88,9 +88,9 @@ locations.
    ```
 
    ```bash
+   > __NOTE:__: for macos change the -w0 option for base64 to -b0
    AZURE_ARM_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-arm-shared \
-     --identifier-uris "https://$(uuidgen)/" \
      --query appId \
      -o tsv)"
    az ad app credential reset \
@@ -117,9 +117,9 @@ locations.
    Now create the application:
 
    ```bash
+   > __NOTE:__: for macos change the -w0 option for base64 to -b0
    AZURE_FP_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-fp-shared \
-     --identifier-uris "https://$(uuidgen)/" \
      --query appId \
      -o tsv)"
    az ad app credential reset \
@@ -141,7 +141,6 @@ locations.
    AZURE_RP_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-rp-shared \
      --end-date '2299-12-31T11:59:59+00:00' \
-     --identifier-uris "https://$(uuidgen)/" \
      --key-type password \
      --password "$AZURE_RP_CLIENT_SECRET" \
      --query appId \
@@ -162,7 +161,6 @@ locations.
    AZURE_GATEWAY_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-gateway-shared \
      --end-date '2299-12-31T11:59:59+00:00' \
-     --identifier-uris "https://$(uuidgen)/" \
      --key-type password \
      --password "$AZURE_GATEWAY_CLIENT_SECRET" \
      --query appId \
@@ -177,7 +175,6 @@ locations.
    AZURE_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-tooling-shared \
      --end-date '2299-12-31T11:59:59+00:00' \
-     --identifier-uris "https://$(uuidgen)/" \
      --key-type password \
      --password "$AZURE_CLIENT_SECRET" \
      --query appId \
@@ -194,26 +191,26 @@ locations.
 
    * Go into the Azure Portal
    * Go to Azure Active Directory
-   * Navigate to the `aro-v4-tooling-shared` app page
+   * Navigate to the `aro-v4-tooling-shared` app registration page
    * Click 'API permissions' in the left side pane
-   * Click 'Microsoft Graph'
    * Click 'Add a permission'.
+   * Click 'Microsoft Graph'
    * Select 'Application permissions'
    * Search for 'Application' and select `Application.ReadWrite.OwnedBy`
    * Click 'Add permissions'
    * This request will need to be approved by a tenant administrator. If you are one, you can click the `Grant admin consent for <name>` button to the right of the `Add a permission` button on the app page
 
-1. Set up the RP role definitions and subscription role assignments in your
-   Azure subscription. This mimics the RBAC that ARM sets up.  With at least
-   `User Access Administrator` permissions on your subscription, do:
+1. Set up the RP role definitions and subscription role assignments in your Azure subscription. The usage of "uuidgen" for fpRoleDefinitionId is simply there to keep from interfering with any linked resources and to create the role net new. This mimics the RBAC that ARM sets up. With at least `User Access Administrator` permissions on your subscription, do:
 
    ```bash
+   LOCATION=<YOUR-REGION>
    az deployment sub create \
      -l $LOCATION \
      --template-file deploy/rbac-development.json \
      --parameters \
        "armServicePrincipalId=$(az ad sp list --filter "appId eq '$AZURE_ARM_CLIENT_ID'" --query '[].objectId' -o tsv)" \
        "fpServicePrincipalId=$(az ad sp list --filter "appId eq '$AZURE_FP_CLIENT_ID'" --query '[].objectId' -o tsv)" \
+       "fpRoleDefinitionId"="$(uuidgen)" \
        "devServicePrincipalId=$(az ad sp list --filter "appId eq '$AZURE_CLIENT_ID'" --query '[].objectId' -o tsv)" \
      >/dev/null
    ```
@@ -230,9 +227,9 @@ locations.
    ```
 
    ```bash
+   > __NOTE:__: for macos change the -w0 option for base64 to -b0
    AZURE_PORTAL_CLIENT_ID="$(az ad app create \
      --display-name aro-v4-portal-shared \
-     --identifier-uris "https://$(uuidgen)/" \
      --reply-urls "https://localhost:8444/callback" \
      --query appId \
      -o tsv)"
@@ -241,8 +238,6 @@ locations.
      --cert "$(base64 -w0 <secrets/portal-client.crt)" >/dev/null
    ```
 
-   TODO: more steps are needed to configure aro-v4-portal-shared.
-
 1. Create an AAD application which will fake up the dbtoken client.
 
    1. Create the application and set `requestedAccessTokenVersion`
@@ -255,6 +250,7 @@ locations.
 
    OBJ_ID="$(az ad app show --id $AZURE_DBTOKEN_CLIENT_ID --query objectId)"
 
+   > __NOTE:__: the graph API requires this to be done from a managed machine
    az rest --method PATCH \
       --uri https://graph.microsoft.com/v1.0/applications/$OBJ_ID/ \
       --body '{"api":{"requestedAccessTokenVersion": 2}}'
@@ -378,7 +374,7 @@ storage account so other people on your team can access it via `make secrets`
 # Environment file
 
 1. Choose the resource group prefix.  The resource group location will be
-   appended to the prefix to make the resource group name.
+   The resource group location will be appended to the prefix to make the resource group name. If a v4-prefixed environment exists in the subscription already, use a unique prefix.
 
    ```bash
    RESOURCEGROUP_PREFIX=v4
@@ -480,7 +476,7 @@ each of the bash functions below.
    import_certs_secrets
    ```
 
-   Note: in production, three additional keys/certificates (rp-mdm, rp-mdsd, and
+   > __NOTE:__: in production, three additional keys/certificates (rp-mdm, rp-mdsd, and
    cluster-mdsd) are also required in the $KEYVAULT_PREFIX-svc key vault.  These
    are client certificates for RP metric and log forwarding (respectively) to
    Geneva.
@@ -512,10 +508,12 @@ each of the bash functions below.
         --file secrets/cluster-logging-int.pem
    ```
 
-   Note: in development, if you don't have valid certs for these, you can just
+   > __NOTE:__: in development, if you don't have valid certs for these, you can just
    upload `localhost.pem` as a placeholder for each of these. This will avoid an
    error stemming from them not existing, but it will result in logging pods
-   crash looping in any clusters you make.
+   crash looping in any clusters you make. Additionally, no gateway resources are 
+   created in development so you should not need to execute the cert import statement
+   for the "-gwy" keyvault.
 
 1. In pre-production (int, e2e) certain certificates are provisioned via keyvault
 integration. These should be rotated and generated in the keyvault itself:
@@ -546,4 +544,4 @@ Development value: secrets/cluster-logging-int.pem
 ## Append Resource Group to Subscription Cleaner DenyList
 
 * We have subscription pruning that takes place routinely and need to add our resource group for the shared rp environment to the `denylist` of the cleaner:
-	* [https://github.com/Azure/ARO-RP/blob/e918d1b87be53a3b3cdf18b674768a6480fb56b8/hack/clean/clean.go#L29](https://github.com/Azure/ARO-RP/blob/e918d1b87be53a3b3cdf18b674768a6480fb56b8/hack/clean/clean.go#L29) 
+   * [https://github.com/Azure/ARO-RP/blob/e918d1b87be53a3b3cdf18b674768a6480fb56b8/hack/clean/clean.go#L29](https://github.com/Azure/ARO-RP/blob/e918d1b87be53a3b3cdf18b674768a6480fb56b8/hack/clean/clean.go#L29) 
diff --git a/hack/validate-imports/validate-imports.go b/hack/validate-imports/validate-imports.go
index 440b29a2f..d799b76f3 100644
--- a/hack/validate-imports/validate-imports.go
+++ b/hack/validate-imports/validate-imports.go
@@ -15,16 +15,6 @@ func isStandardLibrary(path string) bool {
 	return !strings.ContainsRune(strings.SplitN(path, "/", 2)[0], '.')
 }
 
-func validateDotImport(path string) error {
-	switch path {
-	case "github.com/onsi/ginkgo",
-		"github.com/onsi/gomega":
-		return nil
-	}
-
-	return fmt.Errorf("invalid . import %s", path)
-}
-
 func validateUnderscoreImport(path string) error {
 	if regexp.MustCompile(`^github\.com/Azure/ARO-RP/pkg/api/(admin|v[^/]+)$`).MatchString(path) {
 		return nil
@@ -227,10 +217,7 @@ nextImport:
 		value := strings.Trim(imp.Path.Value, `"`)
 
 		if imp.Name != nil && imp.Name.Name == "." {
-			err := validateDotImport(value)
-			if err != nil {
-				errs = append(errs, err)
-			}
+			//accept dotimports because we check them with golangci-lint
 			continue
 		}
 
@@ -279,7 +266,6 @@ nextImport:
 				continue nextImport
 			}
 		}
-
 		errs = append(errs, fmt.Errorf("%s is imported as %q, should be %q", value, imp.Name, names))
 	}
 
diff --git a/pkg/database/generate.go b/pkg/database/generate.go
new file mode 100644
index 000000000..8bf7fb110
--- /dev/null
+++ b/pkg/database/generate.go
@@ -0,0 +1,7 @@
+package database
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+//go:generate go run ../../vendor/github.com/golang/mock/mockgen -destination=../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/$GOPACKAGE Gateway
+//go:generate go run ../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../util/mocks/$GOPACKAGE/$GOPACKAGE.go
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index e20f0f25c..b59783e06 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -5,6 +5,7 @@ package gateway
 
 import (
 	"context"
+	"errors"
 	"log"
 	"net"
 	"net/http"
@@ -102,6 +103,10 @@ func NewGateway(ctx context.Context, env env.Core, baseLog, accessLog *logrus.En
 			return nil, err
 		}
 
+		if u.Hostname() == "" {
+			return nil, errors.New("missing required domain. Ensure the environment has both ActiveDirectoryEndpoint and ResourceManagerEndpoint")
+		}
+
 		domains = append(domains, u.Hostname())
 	}
 
diff --git a/pkg/gateway/metrics.go b/pkg/gateway/metrics.go
index b56d6b7c4..1cb2aea03 100644
--- a/pkg/gateway/metrics.go
+++ b/pkg/gateway/metrics.go
@@ -17,16 +17,20 @@ func (g *gateway) emitMetrics() {
 	defer t.Stop()
 
 	for range t.C {
-		g.m.EmitGauge("gateway.connections.open", atomic.LoadInt64(&g.httpConnections), map[string]string{
-			"protocol": "http",
-		})
+		g._emitMetrics()
+	}
+}
+
+func (g *gateway) _emitMetrics() {
+	g.m.EmitGauge("gateway.connections.open", atomic.LoadInt64(&g.httpConnections), map[string]string{
+		"protocol": "http",
+	})
 
-		g.m.EmitGauge("gateway.connections.open", atomic.LoadInt64(&g.httpsConnections), map[string]string{
-			"protocol": "https",
-		})
+	g.m.EmitGauge("gateway.connections.open", atomic.LoadInt64(&g.httpsConnections), map[string]string{
+		"protocol": "https",
+	})
 
-		if lastChangefeed, ok := g.lastChangefeed.Load().(time.Time); ok {
-			g.m.EmitGauge("gateway.lastchangefeed", lastChangefeed.Unix(), nil)
-		}
+	if lastChangefeed, ok := g.lastChangefeed.Load().(time.Time); ok {
+		g.m.EmitGauge("gateway.lastchangefeed", lastChangefeed.Unix(), nil)
 	}
 }
diff --git a/pkg/operator/controllers/muo/muo_controller.go b/pkg/operator/controllers/muo/muo_controller.go
index 97a3c9e86..9cef6d5a0 100644
--- a/pkg/operator/controllers/muo/muo_controller.go
+++ b/pkg/operator/controllers/muo/muo_controller.go
@@ -141,7 +141,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.
 			return r.deployer.IsReady(ctx)
 		}, timeoutCtx.Done())
 		if err != nil {
-			return reconcile.Result{}, fmt.Errorf("Managed Upgrade Operator deployment timed out on Ready: %w", err)
+			return reconcile.Result{}, fmt.Errorf("managed Upgrade Operator deployment timed out on Ready: %w", err)
 		}
 	} else if strings.EqualFold(managed, "false") {
 		err := r.deployer.Remove(ctx)
diff --git a/pkg/operator/deploy/deploy.go b/pkg/operator/deploy/deploy.go
index 378d16a71..b1b30cf2f 100644
--- a/pkg/operator/deploy/deploy.go
+++ b/pkg/operator/deploy/deploy.go
@@ -322,7 +322,7 @@ func (o *operator) IsReady(ctx context.Context) (bool, error) {
 
 func checkIngressIP(ingressProfiles []api.IngressProfile) (string, error) {
 	if ingressProfiles == nil || len(ingressProfiles) < 1 {
-		return "", errors.New("No Ingress Profiles found")
+		return "", errors.New("no Ingress Profiles found")
 	}
 	ingressIP := ingressProfiles[0].IP
 	if len(ingressProfiles) > 1 {
diff --git a/pkg/portal/cluster/clusteroperators.go b/pkg/portal/cluster/clusteroperators.go
index 89319a172..d293e7e2d 100644
--- a/pkg/portal/cluster/clusteroperators.go
+++ b/pkg/portal/cluster/clusteroperators.go
@@ -25,7 +25,7 @@ func clusterOperatorsInformationFromOperatorList(operators *configv1.ClusterOper
 	}
 
 	for _, co := range operators.Items {
-		var Available configv1.ConditionStatus = configv1.ConditionUnknown
+		var Available = configv1.ConditionUnknown
 
 		for _, cnd := range co.Status.Conditions {
 			if cnd.Type == "Available" {
diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go
index 6669be240..4d2890bb3 100644
--- a/pkg/proxy/proxy.go
+++ b/pkg/proxy/proxy.go
@@ -117,12 +117,12 @@ func (s Server) validateProxyRequest(w http.ResponseWriter, r *http.Request) err
 
 	if r.Method != http.MethodConnect {
 		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
-		return errors.New("Request is not valid, method is not CONNECT")
+		return errors.New("request is not valid, method is not CONNECT")
 	}
 
 	if !s.subnet.Contains(net.ParseIP(ip)) {
 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
-		return errors.New("Request is not allowed, the originating IP is not part of the allowed subnet")
+		return errors.New("request is not allowed, the originating IP is not part of the allowed subnet")
 	}
 
 	return nil
diff --git a/pkg/util/cluster/cluster.go b/pkg/util/cluster/cluster.go
index f5aeb329b..a5867ab6f 100644
--- a/pkg/util/cluster/cluster.go
+++ b/pkg/util/cluster/cluster.go
@@ -195,7 +195,7 @@ func (c *Cluster) Create(ctx context.Context, vnetResourceGroup, clusterName str
 		}
 
 		if result.NameAvailable != nil && !*result.NameAvailable {
-			return fmt.Errorf("Could not generate unique key vault name: %v", result.Reason)
+			return fmt.Errorf("could not generate unique key vault name: %v", result.Reason)
 		}
 	}
 
diff --git a/pkg/util/mocks/database/database.go b/pkg/util/mocks/database/database.go
new file mode 100644
index 000000000..aa7bfd28d
--- /dev/null
+++ b/pkg/util/mocks/database/database.go
@@ -0,0 +1,111 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/Azure/ARO-RP/pkg/database (interfaces: Gateway)
+
+// Package mock_database is a generated GoMock package.
+package mock_database
+
+import (
+	context "context"
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+
+	api "github.com/Azure/ARO-RP/pkg/api"
+	cosmosdb "github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+)
+
+// MockGateway is a mock of Gateway interface.
+type MockGateway struct {
+	ctrl     *gomock.Controller
+	recorder *MockGatewayMockRecorder
+}
+
+// MockGatewayMockRecorder is the mock recorder for MockGateway.
+type MockGatewayMockRecorder struct {
+	mock *MockGateway
+}
+
+// NewMockGateway creates a new mock instance.
+func NewMockGateway(ctrl *gomock.Controller) *MockGateway {
+	mock := &MockGateway{ctrl: ctrl}
+	mock.recorder = &MockGatewayMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockGateway) EXPECT() *MockGatewayMockRecorder {
+	return m.recorder
+}
+
+// ChangeFeed mocks base method.
+func (m *MockGateway) ChangeFeed() cosmosdb.GatewayDocumentIterator {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ChangeFeed")
+	ret0, _ := ret[0].(cosmosdb.GatewayDocumentIterator)
+	return ret0
+}
+
+// ChangeFeed indicates an expected call of ChangeFeed.
+func (mr *MockGatewayMockRecorder) ChangeFeed() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeFeed", reflect.TypeOf((*MockGateway)(nil).ChangeFeed))
+}
+
+// Create mocks base method.
+func (m *MockGateway) Create(arg0 context.Context, arg1 *api.GatewayDocument) (*api.GatewayDocument, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0, arg1)
+	ret0, _ := ret[0].(*api.GatewayDocument)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockGatewayMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockGateway)(nil).Create), arg0, arg1)
+}
+
+// Delete mocks base method.
+func (m *MockGateway) Delete(arg0 context.Context, arg1 *api.GatewayDocument) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockGatewayMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockGateway)(nil).Delete), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockGateway) Get(arg0 context.Context, arg1 string) (*api.GatewayDocument, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(*api.GatewayDocument)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockGatewayMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGateway)(nil).Get), arg0, arg1)
+}
+
+// Patch mocks base method.
+func (m *MockGateway) Patch(arg0 context.Context, arg1 string, arg2 func(*api.GatewayDocument) error) (*api.GatewayDocument, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Patch", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*api.GatewayDocument)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Patch indicates an expected call of Patch.
+func (mr *MockGatewayMockRecorder) Patch(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockGateway)(nil).Patch), arg0, arg1, arg2)
+}


Test patch:
diff --git a/pkg/gateway/gateway_test.go b/pkg/gateway/gateway_test.go
index 87d12baec..d9af821c6 100644
--- a/pkg/gateway/gateway_test.go
+++ b/pkg/gateway/gateway_test.go
@@ -4,11 +4,238 @@ package gateway
 // Licensed under the Apache License 2.0.
 
 import (
+	"context"
 	"testing"
+	"time"
+
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/golang/mock/gomock"
+	"github.com/pires/go-proxyproto"
+	"github.com/sirupsen/logrus"
 
 	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient"
+	mock_database "github.com/Azure/ARO-RP/pkg/util/mocks/database"
+	mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
+	mock_metrics "github.com/Azure/ARO-RP/pkg/util/mocks/metrics"
+	utilnet "github.com/Azure/ARO-RP/pkg/util/net"
 )
 
+func TestNewGateway(t *testing.T) {
+	emptyEnv := &azureclient.AROEnvironment{
+		Environment: azure.Environment{
+			ActiveDirectoryEndpoint:    "",
+			ResourceManagerEndpoint:    "",
+			ContainerRegistryDNSSuffix: "dnsSuffix",
+		},
+	}
+
+	populatedEnv := *emptyEnv
+	populatedEnv.ActiveDirectoryEndpoint = "https://activedirectory/redhattest"
+	populatedEnv.ResourceManagerEndpoint = "https://resourcemanager/redhattest"
+
+	for _, tt := range []struct {
+		name              string
+		gatewayDomains    string
+		acrResourceID     string
+		mocks             func(env *mock_env.MockCore)
+		expectedAllowList []string
+		wantErr           string
+	}{
+		{
+			name: "An environment without the required domains returns an error on creation",
+			mocks: func(env *mock_env.MockCore) {
+				env.EXPECT().Environment().AnyTimes().Return(emptyEnv)
+			},
+			expectedAllowList: make([]string, 0),
+			wantErr:           "missing required domain. Ensure the environment has both ActiveDirectoryEndpoint and ResourceManagerEndpoint",
+		},
+		{
+			name:           "GatewayDomains are split, formatted, and added to allowList",
+			gatewayDomains: "doMain1,Domain2",
+			mocks: func(env *mock_env.MockCore) {
+				env.EXPECT().Environment().AnyTimes().Return(&populatedEnv)
+			},
+			expectedAllowList: []string{"domain1", "domain2", "activedirectory", "resourcemanager"},
+		},
+		{
+			name: "Domains in Environments are parsed and appended to allowList",
+			mocks: func(env *mock_env.MockCore) {
+				env.EXPECT().Environment().AnyTimes().AnyTimes().Return(&populatedEnv)
+			},
+			expectedAllowList: []string{"activedirectory", "resourcemanager"},
+		},
+		{
+			name: "Invalid domain format in environment",
+			mocks: func(env *mock_env.MockCore) {
+				invalidEnv := *emptyEnv
+				invalidEnv.ActiveDirectoryEndpoint = "\r"
+				env.EXPECT().Environment().AnyTimes().AnyTimes().Return(&invalidEnv)
+			},
+			wantErr: `parse "\r": net/url: invalid control character in URL`,
+		},
+		{
+			name:          "When the acrResourceID is present it is parsed into domains and added to allowList",
+			acrResourceID: "/subscriptions/93aeba23-2f76-4307-be82-02921df010cf/resourceGroups/global/providers/Microsoft.ContainerRegistry/registries/resourceName",
+			mocks: func(env *mock_env.MockCore) {
+				env.EXPECT().Environment().AnyTimes().Return(&populatedEnv)
+				env.EXPECT().Location().AnyTimes().Return("location")
+			},
+			expectedAllowList: []string{"resourcename.dnssuffix", "resourcename.location.data.dnssuffix", "activedirectory", "resourcemanager"},
+		},
+		{
+			name:          "When the acrResourceID is present but it is invalid",
+			acrResourceID: "invalidAcrResourceID",
+			mocks: func(env *mock_env.MockCore) {
+				env.EXPECT().Environment().AnyTimes().Return(&populatedEnv)
+				env.EXPECT().Location().AnyTimes().Return("location")
+			},
+			wantErr: "parsing failed for invalidAcrResourceID. Invalid resource Id format",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			ctx := context.Background()
+			controller := gomock.NewController(t)
+			defer controller.Finish()
+
+			baseLog := logrus.NewEntry(logrus.StandardLogger())
+			db := mock_database.NewMockGateway(controller)
+			metrics := mock_metrics.NewMockEmitter(controller)
+
+			httpl, _ := utilnet.Listen("tcp", ":8080", SocketSize)
+			httpsl, _ := utilnet.Listen("tcp", ":8443", SocketSize)
+
+			env := mock_env.NewMockCore(controller)
+			tt.mocks(env)
+
+			gtwy, err := NewGateway(ctx, env, baseLog, baseLog, db, httpsl, httpl, tt.acrResourceID, tt.gatewayDomains, metrics)
+
+			if tt.wantErr != "" {
+				if err == nil {
+					t.Errorf("Expected error: %q but did not receive any errors", tt.wantErr)
+				}
+				if err.Error() != tt.wantErr {
+					t.Errorf("The expected error: %q did not match the actual error: %q", tt.wantErr, err.Error())
+				}
+				return // no need to continue processing the tests
+			} else if err != nil {
+				t.Errorf("Got an unexpected error: %q", err.Error())
+				return
+			}
+
+			gtwyValue, _ := gtwy.(*gateway)
+
+			if len(gtwyValue.allowList) != len(tt.expectedAllowList) {
+				t.Errorf("expected %d but got %d domains in the allow list", len(tt.expectedAllowList), len(gtwyValue.allowList))
+			}
+
+			for _, expectedDomain := range tt.expectedAllowList {
+				_, ok := gtwyValue.allowList[expectedDomain]
+				if !ok {
+					t.Errorf("wanted domain: %q but it was missing", expectedDomain)
+				}
+			}
+		})
+	}
+}
+
+func TestNewGatewayDefaultConditions(t *testing.T) {
+	populatedEnv := &azureclient.AROEnvironment{
+		Environment: azure.Environment{
+			ActiveDirectoryEndpoint:    "https://activedirectory/redhattest",
+			ResourceManagerEndpoint:    "https://resourcemanager/redhattest",
+			ContainerRegistryDNSSuffix: "dnsSuffix",
+		},
+	}
+
+	httpl, _ := utilnet.Listen("tcp", ":8080", SocketSize)
+	httpsl, _ := utilnet.Listen("tcp", ":8443", SocketSize)
+	acrResourceID := "/subscriptions/93aeba23-2f76-4307-be82-02921df010cf/resourceGroups/global/providers/Microsoft.ContainerRegistry/registries/resourceName"
+	gatewayDomains := "doMain1,Domain2"
+	ctx := context.Background()
+	baseLog := logrus.NewEntry(logrus.StandardLogger())
+
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+	db := mock_database.NewMockGateway(controller)
+	metrics := mock_metrics.NewMockEmitter(controller)
+	env := mock_env.NewMockCore(controller)
+	env.EXPECT().Environment().AnyTimes().Return(populatedEnv)
+	env.EXPECT().Location().AnyTimes().Return("location")
+
+	gtwy, _ := NewGateway(ctx, env, baseLog, baseLog, db, httpsl, httpl, acrResourceID, gatewayDomains, metrics)
+
+	gateway, _ := gtwy.(*gateway)
+
+	for _, tt := range []struct {
+		name                           string
+		failedGatewayCreationCondition bool
+		failedGatewayCreationReason    string
+	}{
+		{
+			name:                           "httpl is set",
+			failedGatewayCreationCondition: gateway.httpl.(*proxyproto.Listener).Listener != httpl,
+			failedGatewayCreationReason:    "httpl was not set properly",
+		},
+		{
+			name:                           "httpsl is set",
+			failedGatewayCreationCondition: gateway.httpsl.(*proxyproto.Listener).Listener != httpsl,
+			failedGatewayCreationReason:    "httpsl was not set properly",
+		},
+		{
+			name:                           "Read timeout is set",
+			failedGatewayCreationCondition: gateway.s.ReadTimeout != 10*time.Second,
+			failedGatewayCreationReason:    "gateway http server timeout should be set to 10s",
+		},
+		{
+			name:                           "Idle timeout is set",
+			failedGatewayCreationCondition: gateway.s.IdleTimeout != 2*time.Minute,
+			failedGatewayCreationReason:    "gateway http server timeout should be set to 2m0s",
+		},
+		{
+			name:                           "error log prefix is set",
+			failedGatewayCreationCondition: gateway.s.ErrorLog.Prefix() != "",
+			failedGatewayCreationReason:    "gateway error log prefix should be blank",
+		},
+		{
+			name:                           "error log flags are set to 0",
+			failedGatewayCreationCondition: gateway.s.ErrorLog.Flags() != 0,
+			failedGatewayCreationReason:    "gateway error log flags should be set to 0",
+		},
+		{
+			name:                           "baseContext is the context of the gateway",
+			failedGatewayCreationCondition: gateway.s.BaseContext(httpl) != ctx,
+			failedGatewayCreationReason:    "gateway http server BaseContext should return the ctx of the gateway",
+		},
+		{
+			name:                           "gateway http server handler is set",
+			failedGatewayCreationCondition: gateway.s.Handler == nil,
+			failedGatewayCreationReason:    "gateway http server should have its handler set",
+		},
+		{
+			name:                           "a new gateway has an empty list of gateways",
+			failedGatewayCreationCondition: len(gateway.gateways) != 0,
+			failedGatewayCreationReason:    "gateways should be empty",
+		},
+		{
+			name:                           "gateways metrics is set",
+			failedGatewayCreationCondition: gateway.m != metrics,
+			failedGatewayCreationReason:    "gateway metrics are not set properly",
+		},
+		{
+			name:                           "gateway is ready on creation",
+			failedGatewayCreationCondition: gateway.ready.Load() != true,
+			failedGatewayCreationReason:    "gateway is not ready when it should be",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.failedGatewayCreationCondition {
+				t.Error(tt.failedGatewayCreationReason)
+			}
+		})
+	}
+}
+
 func TestUpdateGateway(t *testing.T) {
 	for _, tt := range []struct {
 		name                   string
diff --git a/pkg/gateway/metrics_test.go b/pkg/gateway/metrics_test.go
new file mode 100644
index 000000000..2bc4f00f2
--- /dev/null
+++ b/pkg/gateway/metrics_test.go
@@ -0,0 +1,57 @@
+package gateway
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+
+	mock_metrics "github.com/Azure/ARO-RP/pkg/util/mocks/metrics"
+)
+
+func TestEmitMetrics(t *testing.T) {
+	testStartTime := time.Now()
+
+	for _, tt := range []struct {
+		name               string
+		httpConnections    int64
+		httpsConnections   int64
+		lastChangefeedTime time.Time
+	}{
+		{
+			name:               "1 http connection 1 https connection no lastChangefeed",
+			httpConnections:    1,
+			httpsConnections:   1,
+			lastChangefeedTime: testStartTime,
+		},
+		{
+			name:               "0 http connection 0 https connections lastChangefeed loads",
+			lastChangefeedTime: time.Date(2022, time.April, 19, 9, 0, 0, 0, time.UTC),
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			mockController := gomock.NewController(t)
+			defer mockController.Finish()
+			mock_metrics := mock_metrics.NewMockEmitter(mockController)
+
+			mock_metrics.EXPECT().EmitGauge("gateway.connections.open", tt.httpConnections, map[string]string{"protocol": "http"}).Times(1)
+			mock_metrics.EXPECT().EmitGauge("gateway.connections.open", tt.httpsConnections, map[string]string{"protocol": "https"}).Times(1)
+
+			gateway := gateway{
+				m:                mock_metrics,
+				httpConnections:  tt.httpConnections,
+				httpsConnections: tt.httpsConnections,
+			}
+
+			if !tt.lastChangefeedTime.Equal(testStartTime) {
+				gateway.lastChangefeed.Store(tt.lastChangefeedTime)
+				mock_metrics.EXPECT().EmitGauge("gateway.lastchangefeed", tt.lastChangefeedTime.Unix(), nil).Times(1)
+			}
+
+			gateway._emitMetrics()
+		})
+	}
+}
diff --git a/pkg/operator/controllers/muo/muo_controller_test.go b/pkg/operator/controllers/muo/muo_controller_test.go
index 4a81a405f..e88119496 100644
--- a/pkg/operator/controllers/muo/muo_controller_test.go
+++ b/pkg/operator/controllers/muo/muo_controller_test.go
@@ -174,7 +174,7 @@ func TestMUOReconciler(t *testing.T) {
 				md.EXPECT().CreateOrUpdate(gomock.Any(), cluster, expectedConfig).Return(nil)
 				md.EXPECT().IsReady(gomock.Any()).Return(false, nil)
 			},
-			wantErr: "Managed Upgrade Operator deployment timed out on Ready: timed out waiting for the condition",
+			wantErr: "managed Upgrade Operator deployment timed out on Ready: timed out waiting for the condition",
 		},
 		{
 			name: "managed, CreateOrUpdate() fails",
diff --git a/pkg/operator/deploy/deploy_test.go b/pkg/operator/deploy/deploy_test.go
index 3241a878a..5b1268653 100644
--- a/pkg/operator/deploy/deploy_test.go
+++ b/pkg/operator/deploy/deploy_test.go
@@ -100,7 +100,7 @@ func TestCheckIngressIP(t *testing.T) {
 				}
 			},
 			want:    "",
-			wantErr: errors.New("No Ingress Profiles found"),
+			wantErr: errors.New("no Ingress Profiles found"),
 		},
 		{
 			name: "Nil IngressProfiles, Error",
@@ -108,7 +108,7 @@ func TestCheckIngressIP(t *testing.T) {
 				return &api.OpenShiftClusterProperties{}
 			},
 			want:    "",
-			wantErr: errors.New("No Ingress Profiles found"),
+			wantErr: errors.New("no Ingress Profiles found"),
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/util/conditions/condition_test.go b/pkg/util/conditions/condition_test.go
index 4b46e6fd6..98ab56992 100644
--- a/pkg/util/conditions/condition_test.go
+++ b/pkg/util/conditions/condition_test.go
@@ -27,7 +27,7 @@ func TestSetCondition(t *testing.T) {
 	version := "unknown"
 
 	kubeclock = &clock.FakeClock{}
-	var transitionTime metav1.Time = metav1.Time{Time: kubeclock.Now()}
+	var transitionTime = metav1.Time{Time: kubeclock.Now()}
 
 	for _, tt := range []struct {
 		name      string
diff --git a/test/database/openshiftclusters.go b/test/database/openshiftclusters.go
index f3df28b0d..ff5eb4f40 100644
--- a/test/database/openshiftclusters.go
+++ b/test/database/openshiftclusters.go
@@ -177,7 +177,7 @@ func (i *fakeOpenShiftClustersQueueLengthIterator) Next(ctx context.Context, max
 
 func (i *fakeOpenShiftClustersQueueLengthIterator) NextRaw(ctx context.Context, continuation int, out interface{}) error {
 	if i.called {
-		return errors.New("Can't call twice!")
+		return errors.New("can't call twice")
 	}
 	i.called = true
 
diff --git a/test/e2e/adminapi_cluster_update.go b/test/e2e/adminapi_cluster_update.go
index 5d4afcfd3..48d65e24e 100644
--- a/test/e2e/adminapi_cluster_update.go
+++ b/test/e2e/adminapi_cluster_update.go
@@ -21,7 +21,7 @@ var _ = Describe("[Admin API] Cluster admin update action", func() {
 	BeforeEach(skipIfNotInDevelopmentEnv)
 
 	It("should be able to run cluster update operation on a cluster", func() {
-		var oc *admin.OpenShiftCluster = &admin.OpenShiftCluster{}
+		var oc = &admin.OpenShiftCluster{}
 		ctx := context.Background()
 		resourceID := resourceIDFromEnv()
 
diff --git a/test/util/project/project.go b/test/util/project/project.go
index d9f58d61e..dc135028f 100644
--- a/test/util/project/project.go
+++ b/test/util/project/project.go
@@ -60,11 +60,11 @@ func (p Project) Verify(ctx context.Context) error {
 
 	sa, err := p.cli.CoreV1().ServiceAccounts(p.name).Get(ctx, "default", metav1.GetOptions{})
 	if err != nil || kerrors.IsNotFound(err) {
-		return fmt.Errorf("Error retrieving default ServiceAccount")
+		return fmt.Errorf("error retrieving default ServiceAccount")
 	}
 
 	if len(sa.Secrets) == 0 {
-		return fmt.Errorf("Default ServiceAccount does not have secrets")
+		return fmt.Errorf("default ServiceAccount does not have secrets")
 	}
 
 	proj, err := p.projectClient.ProjectV1().Projects().Get(ctx, p.name, metav1.GetOptions{})