From c91fd9743acbb724a3163e9391df1e5bcceb9cf1 Mon Sep 17 00:00:00 2001
From: James Page <james@bob.co.za>
Date: Tue, 21 May 2024 15:22:09 +0200
Subject: [PATCH] Add NormalizeEmail and ValidateIPAddress functions

Added function to normalise an email address. Currently setup with the default rules for how some email domains allow plus-addressing and other variations, but we can extend it if we need to.
Added function to validate and clean an IP address.
Added tests for both functions, as well as for StripEmail.
---
 go.mod              |   1 +
 go.sum              |   2 +
 utils/utils.go      |  16 +++++
 utils/utils_test.go | 148 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 167 insertions(+)
 create mode 100644 utils/utils_test.go

diff --git a/go.mod b/go.mod
index abf221b..3078df4 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/aws/aws-lambda-go v1.26.0
 	github.com/aws/aws-sdk-go v1.44.180
 	github.com/aws/aws-secretsmanager-caching-go v1.1.0
+	github.com/dimuska139/go-email-normalizer/v2 v2.0.0
 	github.com/dlsniper/debugger v0.6.0
 	github.com/go-pg/pg/v10 v10.10.6
 	github.com/go-redis/redis/v8 v8.11.4
diff --git a/go.sum b/go.sum
index cc0f301..23b6fde 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dimuska139/go-email-normalizer/v2 v2.0.0 h1:LH41ypO4BFast9bc8hNu6YEkRvLloHFYihSjfwiARSg=
+github.com/dimuska139/go-email-normalizer/v2 v2.0.0/go.mod h1:2Gil1j/rfUKJ5BHc/uxxyRiuk3YTg6/C3D7dz7jVQfw=
 github.com/dlsniper/debugger v0.6.0 h1:AyPoOtJviCmig9AKNRAPPw5B5UyB+cI72zY3Jb+6LlA=
 github.com/dlsniper/debugger v0.6.0/go.mod h1:FFdRcPU2Yo4P411bp5U97DHJUSUMKcqw1QMGUu0uVb8=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
diff --git a/utils/utils.go b/utils/utils.go
index d06280d..526025d 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -3,11 +3,13 @@ package utils
 import (
 	"bytes"
 	emailverifier "github.com/AfterShip/email-verifier"
+	normalizer "github.com/dimuska139/go-email-normalizer/v2"
 	"github.com/jinzhu/now"
 	"github.com/mohae/deepcopy"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
 	"math"
+	"net"
 	"net/url"
 	"os"
 	"reflect"
@@ -149,6 +151,11 @@ func StripEmail(email string) (strippedEmail string, strippedDomain string) {
 	return strippedEmail, strippedDomain
 }
 
+func NormalizeEmail(email string) string {
+	emailNormalizer := normalizer.NewNormalizer()
+	return emailNormalizer.Normalize(email)
+}
+
 // IsUrlStrict Returns whether a URL is valid in a strict way (Must have scheme and host)
 func IsUrlStrict(str string) bool {
 	u, err := url.Parse(str)
@@ -258,3 +265,12 @@ func DetermineDaysLeft(fromDateLocal time.Time, toDateLocal time.Time) int {
 	toDate := now.With(toDateLocal).EndOfDay()
 	return int(math.Floor(toDate.Sub(fromDate).Hours() / 24))
 }
+
+func ValidateIPAddress(ipAddress string) (cleanedIPAddress string, err error) {
+	ipAddress = strings.ToLower(strings.TrimSpace(ipAddress))
+	ip := net.ParseIP(ipAddress)
+	if ip == nil {
+		return "", errors.Error("invalid IP address")
+	}
+	return ipAddress, nil
+}
diff --git a/utils/utils_test.go b/utils/utils_test.go
new file mode 100644
index 0000000..b1a0769
--- /dev/null
+++ b/utils/utils_test.go
@@ -0,0 +1,148 @@
+package utils
+
+import (
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
+	"testing"
+)
+
+func TestStripEmail(t *testing.T) {
+	tests := []struct {
+		name         string
+		email        string
+		wantStripped string
+		wantDomain   string
+	}{
+		{
+			name:         "Test with + symbol",
+			email:        "test+extra@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test without + symbol",
+			email:        "test@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test with multiple + symbols",
+			email:        "test+extra+more@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test with different domain",
+			email:        "test+extra@yahoo.com",
+			wantStripped: "test@yahoo.com",
+			wantDomain:   "yahoo.com",
+		},
+		{
+			name:         "Test with subdomain",
+			email:        "test+extra@mail.example.com",
+			wantStripped: "test@mail.example.com",
+			wantDomain:   "mail.example.com",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotStripped, gotDomain := StripEmail(tt.email)
+			if gotStripped != tt.wantStripped {
+				t.Errorf("StripEmail() gotStripped = %v, want %v", gotStripped, tt.wantStripped)
+			}
+			if gotDomain != tt.wantDomain {
+				t.Errorf("StripEmail() gotDomain = %v, want %v", gotDomain, tt.wantDomain)
+			}
+		})
+	}
+}
+
+func TestNormalizeEmail(t *testing.T) {
+	tests := []struct {
+		name           string
+		email          string
+		wantNormalized string
+	}{
+		{
+			name:           "Test with + symbol",
+			email:          "test+extra@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test without + symbol",
+			email:          "test@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test with multiple + symbols",
+			email:          "test+extra+more@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test with different domain",
+			email:          "test-extra@yahoo.com",
+			wantNormalized: "testextra@yahoo.com",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotNormalized := NormalizeEmail(tt.email)
+			if gotNormalized != tt.wantNormalized {
+				t.Errorf("NormalizeEmail() gotNormalized = %v, want %v", gotNormalized, tt.wantNormalized)
+			}
+		})
+	}
+}
+
+func TestValidateIPAddress(t *testing.T) {
+	tests := []struct {
+		name string
+		ip   string
+		want string
+		err  error
+	}{
+		{
+			name: "Test with valid IPv4",
+			ip:   "192.168.1.1",
+			want: "192.168.1.1",
+			err:  nil,
+		},
+		{
+			name: "Test with valid IPv6",
+			ip:   "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			want: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			err:  nil,
+		},
+		{
+			name: "Test with invalid IP",
+			ip:   "999.999.999.999",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+		{
+			name: "Test with empty string",
+			ip:   "",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+		{
+			name: "Test with non-IP string",
+			ip:   "not an ip address",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ValidateIPAddress(tt.ip)
+			if got != tt.want {
+				t.Errorf("ValidateIPAddress() got = %v, want %v", got, tt.want)
+			}
+			if err != nil && err.Error() != tt.err.Error() {
+				t.Errorf("ValidateIPAddress() err = %v, want %v", err, tt.err)
+			}
+		})
+	}
+}
-- 
GitLab