From 50d8e37a45dc87ba9aecabbc77f63bbe9f668d0e Mon Sep 17 00:00:00 2001
From: Lev <1187448+levb@users.noreply.github.com>
Date: Mon, 22 Jun 2020 09:23:15 -0700
Subject: [PATCH] Fix `/autolink test` command for disabled links (#123)

Also:
- Started a "library" of domain-specific tests, to be used as templates for configuration
- Added a template for ProductBoard
- Fixed a lint nit in client.go
---
 server/autolink/autolink_test.go              | 356 +++---------------
 server/autolink/lib_credit_card_test.go       | 142 +++++++
 server/autolink/lib_jira_test.go              | 125 ++++++
 server/autolink/lib_productboard_test.go      |  41 ++
 .../lib_social_security_number_test.go        |  41 ++
 server/autolinkclient/client.go               |   2 +-
 server/autolinkplugin/command.go              |  10 +-
 7 files changed, 401 insertions(+), 316 deletions(-)
 create mode 100644 server/autolink/lib_credit_card_test.go
 create mode 100644 server/autolink/lib_jira_test.go
 create mode 100644 server/autolink/lib_productboard_test.go
 create mode 100644 server/autolink/lib_social_security_number_test.go

diff --git a/server/autolink/autolink_test.go b/server/autolink/autolink_test.go
index baa8d172..b40d7eaa 100644
--- a/server/autolink/autolink_test.go
+++ b/server/autolink/autolink_test.go
@@ -2,7 +2,6 @@ package autolink_test
 
 import (
 	"fmt"
-	"regexp"
 	"testing"
 
 	"github.com/mattermost/mattermost-server/v5/model"
@@ -42,322 +41,51 @@ func setupTestPlugin(t *testing.T, l autolink.Autolink) *autolinkplugin.Plugin {
 	return p
 }
 
-const (
-	reVISA       = `(?P<VISA>(?P<part1>4\d{3})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
-	reMasterCard = `(?P<MasterCard>(?P<part1>5[1-5]\d{2})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
-	reSwitchSolo = `(?P<SwitchSolo>(?P<part1>67\d{2})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
-	reDiscover   = `(?P<Discover>(?P<part1>6011)[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
-	reAMEX       = `(?P<AMEX>(?P<part1>3[47]\d{2})[ -]?(?P<part2>\d{6})[ -]?(?P<part3>\d)(?P<LastFour>[0-9]{4}))`
-
-	replaceVISA       = "VISA XXXX-XXXX-XXXX-$LastFour"
-	replaceMasterCard = "MasterCard XXXX-XXXX-XXXX-$LastFour"
-	replaceSwitchSolo = "Switch/Solo XXXX-XXXX-XXXX-$LastFour"
-	replaceDiscover   = "Discover XXXX-XXXX-XXXX-$LastFour"
-	replaceAMEX       = "American Express XXXX-XXXXXX-X$LastFour"
-)
-
-func TestCCRegex(t *testing.T) {
-	for _, tc := range []struct {
-		Name    string
-		RE      string
-		Replace string
-		In      string
-		Out     string
-	}{
-		{"Visa happy spaces", reVISA, replaceVISA, " abc 4111 1111 1111 1234 def", " abc VISA XXXX-XXXX-XXXX-1234 def"},
-		{"Visa happy dashes", reVISA, replaceVISA, "4111-1111-1111-1234", "VISA XXXX-XXXX-XXXX-1234"},
-		{"Visa happy mixed", reVISA, replaceVISA, "41111111 1111-1234", "VISA XXXX-XXXX-XXXX-1234"},
-		{"Visa happy digits", reVISA, replaceVISA, "abc 4111111111111234 def", "abc VISA XXXX-XXXX-XXXX-1234 def"},
-		{"Visa non-match start", reVISA, replaceVISA, "3111111111111234", ""},
-		{"Visa non-match num digits", reVISA, replaceVISA, " 4111-1111-1111-123", ""},
-		{"Visa non-match sep", reVISA, replaceVISA, "4111=1111=1111_1234", ""},
-		{"Visa non-match no break before", reVISA, replaceVISA, "abc4111-1111-1111-1234", "abcVISA XXXX-XXXX-XXXX-1234"},
-		{"Visa non-match no break after", reVISA, replaceVISA, "4111-1111-1111-1234def", "VISA XXXX-XXXX-XXXX-1234def"},
-
-		{"MasterCard happy spaces", reMasterCard, replaceMasterCard, " abc 5111 1111 1111 1234 def", " abc MasterCard XXXX-XXXX-XXXX-1234 def"},
-		{"MasterCard happy dashes", reMasterCard, replaceMasterCard, "5211-1111-1111-1234", "MasterCard XXXX-XXXX-XXXX-1234"},
-		{"MasterCard happy mixed", reMasterCard, replaceMasterCard, "53111111 1111-1234", "MasterCard XXXX-XXXX-XXXX-1234"},
-		{"MasterCard happy digits", reMasterCard, replaceMasterCard, "abc 5411111111111234 def", "abc MasterCard XXXX-XXXX-XXXX-1234 def"},
-		{"MasterCard non-match start", reMasterCard, replaceMasterCard, "3111111111111234", ""},
-		{"MasterCard non-match num digits", reMasterCard, replaceMasterCard, " 5111-1111-1111-123", ""},
-		{"MasterCard non-match sep", reMasterCard, replaceMasterCard, "5111=1111=1111_1234", ""},
-		{"MasterCard non-match no break before", reMasterCard, replaceMasterCard, "abc5511-1111-1111-1234", "abcMasterCard XXXX-XXXX-XXXX-1234"},
-		{"MasterCard non-match no break after", reMasterCard, replaceMasterCard, "5111-1111-1111-1234def", "MasterCard XXXX-XXXX-XXXX-1234def"},
-
-		{"SwitchSolo happy spaces", reSwitchSolo, replaceSwitchSolo, " abc 6711 1111 1111 1234 def", " abc Switch/Solo XXXX-XXXX-XXXX-1234 def"},
-		{"SwitchSolo happy dashes", reSwitchSolo, replaceSwitchSolo, "6711-1111-1111-1234", "Switch/Solo XXXX-XXXX-XXXX-1234"},
-		{"SwitchSolo happy mixed", reSwitchSolo, replaceSwitchSolo, "67111111 1111-1234", "Switch/Solo XXXX-XXXX-XXXX-1234"},
-		{"SwitchSolo happy digits", reSwitchSolo, replaceSwitchSolo, "abc 6711111111111234 def", "abc Switch/Solo XXXX-XXXX-XXXX-1234 def"},
-		{"SwitchSolo non-match start", reSwitchSolo, replaceSwitchSolo, "3111111111111234", ""},
-		{"SwitchSolo non-match num digits", reSwitchSolo, replaceSwitchSolo, " 6711-1111-1111-123", ""},
-		{"SwitchSolo non-match sep", reSwitchSolo, replaceSwitchSolo, "6711=1111=1111_1234", ""},
-		{"SwitchSolo non-match no break before", reSwitchSolo, replaceSwitchSolo, "abc6711-1111-1111-1234", "abcSwitch/Solo XXXX-XXXX-XXXX-1234"},
-		{"SwitchSolo non-match no break after", reSwitchSolo, replaceSwitchSolo, "6711-1111-1111-1234def", "Switch/Solo XXXX-XXXX-XXXX-1234def"},
-
-		{"Discover happy spaces", reDiscover, replaceDiscover, " abc 6011 1111 1111 1234 def", " abc Discover XXXX-XXXX-XXXX-1234 def"},
-		{"Discover happy dashes", reDiscover, replaceDiscover, "6011-1111-1111-1234", "Discover XXXX-XXXX-XXXX-1234"},
-		{"Discover happy mixed", reDiscover, replaceDiscover, "60111111 1111-1234", "Discover XXXX-XXXX-XXXX-1234"},
-		{"Discover happy digits", reDiscover, replaceDiscover, "abc 6011111111111234 def", "abc Discover XXXX-XXXX-XXXX-1234 def"},
-		{"Discover non-match start", reDiscover, replaceDiscover, "3111111111111234", ""},
-		{"Discover non-match num digits", reDiscover, replaceDiscover, " 6011-1111-1111-123", ""},
-		{"Discover non-match sep", reDiscover, replaceDiscover, "6011=1111=1111_1234", ""},
-		{"Discover non-match no break before", reDiscover, replaceDiscover, "abc6011-1111-1111-1234", "abcDiscover XXXX-XXXX-XXXX-1234"},
-		{"Discover non-match no break after", reDiscover, replaceDiscover, "6011-1111-1111-1234def", "Discover XXXX-XXXX-XXXX-1234def"},
-
-		{"AMEX happy spaces", reAMEX, replaceAMEX, " abc 3411 123456 12345 def", " abc American Express XXXX-XXXXXX-X2345 def"},
-		{"AMEX happy dashes", reAMEX, replaceAMEX, "3711-123456-12345", "American Express XXXX-XXXXXX-X2345"},
-		{"AMEX happy mixed", reAMEX, replaceAMEX, "3411-123456 12345", "American Express XXXX-XXXXXX-X2345"},
-		{"AMEX happy digits", reAMEX, replaceAMEX, "abc 371112345612345 def", "abc American Express XXXX-XXXXXX-X2345 def"},
-		{"AMEX non-match start 41", reAMEX, replaceAMEX, "411112345612345", ""},
-		{"AMEX non-match start 31", reAMEX, replaceAMEX, "3111111111111234", ""},
-		{"AMEX non-match num digits", reAMEX, replaceAMEX, " 4111-1111-1111-123", ""},
-		{"AMEX non-match sep", reAMEX, replaceAMEX, "4111-1111=1111-1234", ""},
-		{"AMEX non-match no break before", reAMEX, replaceAMEX, "abc3711-123456-12345", "abcAmerican Express XXXX-XXXXXX-X2345"},
-		{"AMEX non-match no break after", reAMEX, replaceAMEX, "3711-123456-12345def", "American Express XXXX-XXXXXX-X2345def"},
-	} {
-		t.Run(tc.Name, func(t *testing.T) {
-			re := regexp.MustCompile(tc.RE)
-			result := re.ReplaceAllString(tc.In, tc.Replace)
-			if tc.Out != "" {
-				assert.Equal(t, tc.Out, result)
-			} else {
-				assert.Equal(t, tc.In, result)
-			}
-		})
-	}
+type linkTest struct {
+	Name            string
+	Link            autolink.Autolink
+	Message         string
+	ExpectedMessage string
 }
 
-const (
-	reSSN      = `(?P<SSN>(?P<part1>\d{3})[ -]?(?P<part2>\d{2})[ -]?(?P<LastFour>[0-9]{4}))`
-	replaceSSN = `XXX-XX-$LastFour`
-)
-
-func TestSSNRegex(t *testing.T) {
-	for _, tc := range []struct {
-		Name    string
-		RE      string
-		Replace string
-		In      string
-		Out     string
-	}{
-		{"SSN happy spaces", reSSN, replaceSSN, " abc 652 47 3356 def", " abc XXX-XX-3356 def"},
-		{"SSN happy dashes", reSSN, replaceSSN, " abc 652-47-3356 def", " abc XXX-XX-3356 def"},
-		{"SSN happy digits", reSSN, replaceSSN, " abc 652473356 def", " abc XXX-XX-3356 def"},
-		{"SSN happy mixed1", reSSN, replaceSSN, " abc 65247-3356 def", " abc XXX-XX-3356 def"},
-		{"SSN happy mixed2", reSSN, replaceSSN, " abc 652 47-3356 def", " abc XXX-XX-3356 def"},
-		{"SSN non-match 19-09-9999", reSSN, replaceSSN, " abc 19-09-9999 def", " abc 19-09-9999 def"},
-		{"SSN non-match 652_47-3356", reSSN, replaceSSN, " abc 652_47-3356 def", " abc 652_47-3356 def"},
-	} {
-		t.Run(tc.Name, func(t *testing.T) {
-			re := regexp.MustCompile(tc.RE)
-			result := re.ReplaceAllString(tc.In, tc.Replace)
-			if tc.Out != "" {
-				assert.Equal(t, tc.Out, result)
-			} else {
-				assert.Equal(t, tc.In, result)
-			}
-		})
-	}
-}
-
-func TestCreditCard(t *testing.T) {
-	var tests = []struct {
-		Name            string
-		Link            autolink.Autolink
-		inputMessage    string
-		expectedMessage string
-	}{
-		{
-			"VISA happy",
-			autolink.Autolink{
-				Pattern:  reVISA,
-				Template: replaceVISA,
-			},
-			"A credit card 4111-1111-2222-1234 mentioned",
-			"A credit card VISA XXXX-XXXX-XXXX-1234 mentioned",
-		}, {
-			"VISA",
-			autolink.Autolink{
-				Pattern:              reVISA,
-				Template:             replaceVISA,
-				DisableNonWordPrefix: true,
-				DisableNonWordSuffix: true,
-			},
-			"A credit card4111-1111-2222-3333mentioned",
-			"A credit cardVISA XXXX-XXXX-XXXX-3333mentioned",
-		}, {
-			"Multiple VISA replacements",
-			autolink.Autolink{
-				Pattern:  reVISA,
-				Template: replaceVISA,
-			},
-			"Credit cards 4111-1111-2222-3333 4222-3333-4444-5678 mentioned",
-			"Credit cards VISA XXXX-XXXX-XXXX-3333 VISA XXXX-XXXX-XXXX-5678 mentioned",
+var commonLinkTests = []linkTest{
+	{
+		"Simple pattern",
+		autolink.Autolink{
+			Pattern:  "(Mattermost)",
+			Template: "[Mattermost](https://mattermost.com)",
 		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.Name, func(t *testing.T) {
-			err := tt.Link.Compile()
-			actual := tt.Link.Replace(tt.inputMessage)
-
-			assert.Equal(t, tt.expectedMessage, actual)
-			assert.NoError(t, err)
-		})
-	}
+		"Welcome to Mattermost!",
+		"Welcome to [Mattermost](https://mattermost.com)!",
+	}, {
+		"Pattern with variable name accessed using $variable",
+		autolink.Autolink{
+			Pattern:  "(?P<key>Mattermost)",
+			Template: "[$key](https://mattermost.com)",
+		},
+		"Welcome to Mattermost!",
+		"Welcome to [Mattermost](https://mattermost.com)!",
+	}, {
+		"Multiple replacments",
+		autolink.Autolink{
+			Pattern:  "(?P<key>Mattermost)",
+			Template: "[$key](https://mattermost.com)",
+		},
+		"Welcome to Mattermost and have fun with Mattermost!",
+		"Welcome to [Mattermost](https://mattermost.com) and have fun with [Mattermost](https://mattermost.com)!",
+	}, {
+		"Pattern with variable name accessed using ${variable}",
+		autolink.Autolink{
+			Pattern:  "(?P<key>Mattermost)",
+			Template: "[${key}](https://mattermost.com)",
+		},
+		"Welcome to Mattermost!",
+		"Welcome to [Mattermost](https://mattermost.com)!",
+	},
 }
 
-func TestLink(t *testing.T) {
-	for _, tc := range []struct {
-		Name            string
-		Link            autolink.Autolink
-		Message         string
-		ExpectedMessage string
-	}{
-		{
-			"Simple pattern",
-			autolink.Autolink{
-				Pattern:  "(Mattermost)",
-				Template: "[Mattermost](https://mattermost.com)",
-			},
-			"Welcome to Mattermost!",
-			"Welcome to [Mattermost](https://mattermost.com)!",
-		}, {
-			"Pattern with variable name accessed using $variable",
-			autolink.Autolink{
-				Pattern:  "(?P<key>Mattermost)",
-				Template: "[$key](https://mattermost.com)",
-			},
-			"Welcome to Mattermost!",
-			"Welcome to [Mattermost](https://mattermost.com)!",
-		}, {
-			"Multiple replacments",
-			autolink.Autolink{
-				Pattern:  "(?P<key>Mattermost)",
-				Template: "[$key](https://mattermost.com)",
-			},
-			"Welcome to Mattermost and have fun with Mattermost!",
-			"Welcome to [Mattermost](https://mattermost.com) and have fun with [Mattermost](https://mattermost.com)!",
-		}, {
-			"Pattern with variable name accessed using ${variable}",
-			autolink.Autolink{
-				Pattern:  "(?P<key>Mattermost)",
-				Template: "[${key}](https://mattermost.com)",
-			},
-			"Welcome to Mattermost!",
-			"Welcome to [Mattermost](https://mattermost.com)!",
-		}, {
-			"Jira example",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Welcome MM-12345 should link!",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
-		}, {
-			"Jira example 2 (within a ())",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Link in brackets should link (see MM-12345)",
-			"Link in brackets should link (see [MM-12345](https://mattermost.atlassian.net/browse/MM-12345))",
-		}, {
-			"Jira example 3 (before ,)",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Link a ticket MM-12345, before a comma",
-			"Link a ticket [MM-12345](https://mattermost.atlassian.net/browse/MM-12345), before a comma",
-		}, {
-			"Jira example 3 (at begin of the message)",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"MM-12345 should link!",
-			"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
-		}, {
-			"Pattern word prefix and suffix disabled",
-			autolink.Autolink{
-				Pattern:              "(?P<previous>^|\\s)(MM)(-)(?P<jira_id>\\d+)",
-				Template:             "${previous}[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-				DisableNonWordPrefix: true,
-				DisableNonWordSuffix: true,
-			},
-			"Welcome MM-12345 should link!",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
-		}, {
-			"Pattern word prefix and suffix disabled (at begin of the message)",
-			autolink.Autolink{
-				Pattern:              "(?P<previous>^|\\s)(MM)(-)(?P<jira_id>\\d+)",
-				Template:             "${previous}[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-				DisableNonWordPrefix: true,
-				DisableNonWordSuffix: true,
-			},
-			"MM-12345 should link!",
-			"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
-		}, {
-			"Pattern word prefix and suffix enable (in the middle of other text)",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"WelcomeMM-12345should not link!",
-			"WelcomeMM-12345should not link!",
-		}, {
-			"Pattern word prefix and suffix disabled (in the middle of other text)",
-			autolink.Autolink{
-				Pattern:              "(MM)(-)(?P<jira_id>\\d+)",
-				Template:             "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-				DisableNonWordPrefix: true,
-				DisableNonWordSuffix: true,
-			},
-			"WelcomeMM-12345should link!",
-			"Welcome[MM-12345](https://mattermost.atlassian.net/browse/MM-12345)should link!",
-		}, {
-			"Not relinking",
-			autolink.Autolink{
-				Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should not re-link!",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should not re-link!",
-		}, {
-			"Url replacement",
-			autolink.Autolink{
-				Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Welcome https://mattermost.atlassian.net/browse/MM-12345 should link!",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
-		}, {
-			"Url replacement multiple times",
-			autolink.Autolink{
-				Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Welcome https://mattermost.atlassian.net/browse/MM-12345. should link https://mattermost.atlassian.net/browse/MM-12346 !",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345). should link [MM-12346](https://mattermost.atlassian.net/browse/MM-12346) !",
-		}, {
-			"Url replacement multiple times and at beginning",
-			autolink.Autolink{
-				Pattern:  "(https:\\/\\/mattermost.atlassian.net\\/browse\\/)(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"https://mattermost.atlassian.net/browse/MM-12345 https://mattermost.atlassian.net/browse/MM-12345",
-			"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) [MM-12345](https://mattermost.atlassian.net/browse/MM-12345)",
-		}, {
-			"Url replacement at end",
-			autolink.Autolink{
-				Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
-				Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
-			},
-			"Welcome https://mattermost.atlassian.net/browse/MM-12345",
-			"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345)",
-		},
-	} {
+func testLinks(t *testing.T, tcs ...linkTest) {
+	for _, tc := range tcs {
 		t.Run(tc.Name, func(t *testing.T) {
 			p := setupTestPlugin(t, tc.Link)
 			post, _ := p.MessageWillBePosted(nil, &model.Post{
@@ -369,6 +97,10 @@ func TestLink(t *testing.T) {
 	}
 }
 
+func TestCommonLinks(t *testing.T) {
+	testLinks(t, commonLinkTests...)
+}
+
 func TestLegacyWordBoundaries(t *testing.T) {
 	const pattern = "(KEY)(-)(?P<ID>\\d+)"
 	const template = "[KEY-$ID](someurl/KEY-$ID)"
diff --git a/server/autolink/lib_credit_card_test.go b/server/autolink/lib_credit_card_test.go
new file mode 100644
index 00000000..98004c17
--- /dev/null
+++ b/server/autolink/lib_credit_card_test.go
@@ -0,0 +1,142 @@
+package autolink_test
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/mattermost/mattermost-plugin-autolink/server/autolink"
+)
+
+const (
+	reVISA       = `(?P<VISA>(?P<part1>4\d{3})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
+	reMasterCard = `(?P<MasterCard>(?P<part1>5[1-5]\d{2})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
+	reSwitchSolo = `(?P<SwitchSolo>(?P<part1>67\d{2})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
+	reDiscover   = `(?P<Discover>(?P<part1>6011)[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))`
+	reAMEX       = `(?P<AMEX>(?P<part1>3[47]\d{2})[ -]?(?P<part2>\d{6})[ -]?(?P<part3>\d)(?P<LastFour>[0-9]{4}))`
+
+	replaceVISA       = "VISA XXXX-XXXX-XXXX-$LastFour"
+	replaceMasterCard = "MasterCard XXXX-XXXX-XXXX-$LastFour"
+	replaceSwitchSolo = "Switch/Solo XXXX-XXXX-XXXX-$LastFour"
+	replaceDiscover   = "Discover XXXX-XXXX-XXXX-$LastFour"
+	replaceAMEX       = "American Express XXXX-XXXXXX-X$LastFour"
+)
+
+func TestCreditCardRegex(t *testing.T) {
+	for _, tc := range []struct {
+		Name    string
+		RE      string
+		Replace string
+		In      string
+		Out     string
+	}{
+		{"Visa happy spaces", reVISA, replaceVISA, " abc 4111 1111 1111 1234 def", " abc VISA XXXX-XXXX-XXXX-1234 def"},
+		{"Visa happy dashes", reVISA, replaceVISA, "4111-1111-1111-1234", "VISA XXXX-XXXX-XXXX-1234"},
+		{"Visa happy mixed", reVISA, replaceVISA, "41111111 1111-1234", "VISA XXXX-XXXX-XXXX-1234"},
+		{"Visa happy digits", reVISA, replaceVISA, "abc 4111111111111234 def", "abc VISA XXXX-XXXX-XXXX-1234 def"},
+		{"Visa non-match start", reVISA, replaceVISA, "3111111111111234", ""},
+		{"Visa non-match num digits", reVISA, replaceVISA, " 4111-1111-1111-123", ""},
+		{"Visa non-match sep", reVISA, replaceVISA, "4111=1111=1111_1234", ""},
+		{"Visa non-match no break before", reVISA, replaceVISA, "abc4111-1111-1111-1234", "abcVISA XXXX-XXXX-XXXX-1234"},
+		{"Visa non-match no break after", reVISA, replaceVISA, "4111-1111-1111-1234def", "VISA XXXX-XXXX-XXXX-1234def"},
+
+		{"MasterCard happy spaces", reMasterCard, replaceMasterCard, " abc 5111 1111 1111 1234 def", " abc MasterCard XXXX-XXXX-XXXX-1234 def"},
+		{"MasterCard happy dashes", reMasterCard, replaceMasterCard, "5211-1111-1111-1234", "MasterCard XXXX-XXXX-XXXX-1234"},
+		{"MasterCard happy mixed", reMasterCard, replaceMasterCard, "53111111 1111-1234", "MasterCard XXXX-XXXX-XXXX-1234"},
+		{"MasterCard happy digits", reMasterCard, replaceMasterCard, "abc 5411111111111234 def", "abc MasterCard XXXX-XXXX-XXXX-1234 def"},
+		{"MasterCard non-match start", reMasterCard, replaceMasterCard, "3111111111111234", ""},
+		{"MasterCard non-match num digits", reMasterCard, replaceMasterCard, " 5111-1111-1111-123", ""},
+		{"MasterCard non-match sep", reMasterCard, replaceMasterCard, "5111=1111=1111_1234", ""},
+		{"MasterCard non-match no break before", reMasterCard, replaceMasterCard, "abc5511-1111-1111-1234", "abcMasterCard XXXX-XXXX-XXXX-1234"},
+		{"MasterCard non-match no break after", reMasterCard, replaceMasterCard, "5111-1111-1111-1234def", "MasterCard XXXX-XXXX-XXXX-1234def"},
+
+		{"SwitchSolo happy spaces", reSwitchSolo, replaceSwitchSolo, " abc 6711 1111 1111 1234 def", " abc Switch/Solo XXXX-XXXX-XXXX-1234 def"},
+		{"SwitchSolo happy dashes", reSwitchSolo, replaceSwitchSolo, "6711-1111-1111-1234", "Switch/Solo XXXX-XXXX-XXXX-1234"},
+		{"SwitchSolo happy mixed", reSwitchSolo, replaceSwitchSolo, "67111111 1111-1234", "Switch/Solo XXXX-XXXX-XXXX-1234"},
+		{"SwitchSolo happy digits", reSwitchSolo, replaceSwitchSolo, "abc 6711111111111234 def", "abc Switch/Solo XXXX-XXXX-XXXX-1234 def"},
+		{"SwitchSolo non-match start", reSwitchSolo, replaceSwitchSolo, "3111111111111234", ""},
+		{"SwitchSolo non-match num digits", reSwitchSolo, replaceSwitchSolo, " 6711-1111-1111-123", ""},
+		{"SwitchSolo non-match sep", reSwitchSolo, replaceSwitchSolo, "6711=1111=1111_1234", ""},
+		{"SwitchSolo non-match no break before", reSwitchSolo, replaceSwitchSolo, "abc6711-1111-1111-1234", "abcSwitch/Solo XXXX-XXXX-XXXX-1234"},
+		{"SwitchSolo non-match no break after", reSwitchSolo, replaceSwitchSolo, "6711-1111-1111-1234def", "Switch/Solo XXXX-XXXX-XXXX-1234def"},
+
+		{"Discover happy spaces", reDiscover, replaceDiscover, " abc 6011 1111 1111 1234 def", " abc Discover XXXX-XXXX-XXXX-1234 def"},
+		{"Discover happy dashes", reDiscover, replaceDiscover, "6011-1111-1111-1234", "Discover XXXX-XXXX-XXXX-1234"},
+		{"Discover happy mixed", reDiscover, replaceDiscover, "60111111 1111-1234", "Discover XXXX-XXXX-XXXX-1234"},
+		{"Discover happy digits", reDiscover, replaceDiscover, "abc 6011111111111234 def", "abc Discover XXXX-XXXX-XXXX-1234 def"},
+		{"Discover non-match start", reDiscover, replaceDiscover, "3111111111111234", ""},
+		{"Discover non-match num digits", reDiscover, replaceDiscover, " 6011-1111-1111-123", ""},
+		{"Discover non-match sep", reDiscover, replaceDiscover, "6011=1111=1111_1234", ""},
+		{"Discover non-match no break before", reDiscover, replaceDiscover, "abc6011-1111-1111-1234", "abcDiscover XXXX-XXXX-XXXX-1234"},
+		{"Discover non-match no break after", reDiscover, replaceDiscover, "6011-1111-1111-1234def", "Discover XXXX-XXXX-XXXX-1234def"},
+
+		{"AMEX happy spaces", reAMEX, replaceAMEX, " abc 3411 123456 12345 def", " abc American Express XXXX-XXXXXX-X2345 def"},
+		{"AMEX happy dashes", reAMEX, replaceAMEX, "3711-123456-12345", "American Express XXXX-XXXXXX-X2345"},
+		{"AMEX happy mixed", reAMEX, replaceAMEX, "3411-123456 12345", "American Express XXXX-XXXXXX-X2345"},
+		{"AMEX happy digits", reAMEX, replaceAMEX, "abc 371112345612345 def", "abc American Express XXXX-XXXXXX-X2345 def"},
+		{"AMEX non-match start 41", reAMEX, replaceAMEX, "411112345612345", ""},
+		{"AMEX non-match start 31", reAMEX, replaceAMEX, "3111111111111234", ""},
+		{"AMEX non-match num digits", reAMEX, replaceAMEX, " 4111-1111-1111-123", ""},
+		{"AMEX non-match sep", reAMEX, replaceAMEX, "4111-1111=1111-1234", ""},
+		{"AMEX non-match no break before", reAMEX, replaceAMEX, "abc3711-123456-12345", "abcAmerican Express XXXX-XXXXXX-X2345"},
+		{"AMEX non-match no break after", reAMEX, replaceAMEX, "3711-123456-12345def", "American Express XXXX-XXXXXX-X2345def"},
+	} {
+		t.Run(tc.Name, func(t *testing.T) {
+			re := regexp.MustCompile(tc.RE)
+			result := re.ReplaceAllString(tc.In, tc.Replace)
+			if tc.Out != "" {
+				assert.Equal(t, tc.Out, result)
+			} else {
+				assert.Equal(t, tc.In, result)
+			}
+		})
+	}
+}
+
+func TestCreditCard(t *testing.T) {
+	var tests = []struct {
+		Name            string
+		Link            autolink.Autolink
+		inputMessage    string
+		expectedMessage string
+	}{
+		{
+			"VISA happy",
+			autolink.Autolink{
+				Pattern:  reVISA,
+				Template: replaceVISA,
+			},
+			"A credit card 4111-1111-2222-1234 mentioned",
+			"A credit card VISA XXXX-XXXX-XXXX-1234 mentioned",
+		}, {
+			"VISA",
+			autolink.Autolink{
+				Pattern:              reVISA,
+				Template:             replaceVISA,
+				DisableNonWordPrefix: true,
+				DisableNonWordSuffix: true,
+			},
+			"A credit card4111-1111-2222-3333mentioned",
+			"A credit cardVISA XXXX-XXXX-XXXX-3333mentioned",
+		}, {
+			"Multiple VISA replacements",
+			autolink.Autolink{
+				Pattern:  reVISA,
+				Template: replaceVISA,
+			},
+			"Credit cards 4111-1111-2222-3333 4222-3333-4444-5678 mentioned",
+			"Credit cards VISA XXXX-XXXX-XXXX-3333 VISA XXXX-XXXX-XXXX-5678 mentioned",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.Name, func(t *testing.T) {
+			err := tt.Link.Compile()
+			actual := tt.Link.Replace(tt.inputMessage)
+
+			assert.Equal(t, tt.expectedMessage, actual)
+			assert.NoError(t, err)
+		})
+	}
+}
diff --git a/server/autolink/lib_jira_test.go b/server/autolink/lib_jira_test.go
new file mode 100644
index 00000000..296fc123
--- /dev/null
+++ b/server/autolink/lib_jira_test.go
@@ -0,0 +1,125 @@
+package autolink_test
+
+import (
+	"testing"
+
+	"github.com/mattermost/mattermost-plugin-autolink/server/autolink"
+)
+
+var jiraTests = []linkTest{
+	{
+		"Jira example",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Welcome MM-12345 should link!",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
+	}, {
+		"Jira example 2 (within a ())",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Link in brackets should link (see MM-12345)",
+		"Link in brackets should link (see [MM-12345](https://mattermost.atlassian.net/browse/MM-12345))",
+	}, {
+		"Jira example 3 (before ,)",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Link a ticket MM-12345, before a comma",
+		"Link a ticket [MM-12345](https://mattermost.atlassian.net/browse/MM-12345), before a comma",
+	}, {
+		"Jira example 3 (at begin of the message)",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"MM-12345 should link!",
+		"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
+	}, {
+		"Pattern word prefix and suffix disabled",
+		autolink.Autolink{
+			Pattern:              "(?P<previous>^|\\s)(MM)(-)(?P<jira_id>\\d+)",
+			Template:             "${previous}[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+			DisableNonWordPrefix: true,
+			DisableNonWordSuffix: true,
+		},
+		"Welcome MM-12345 should link!",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
+	}, {
+		"Pattern word prefix and suffix disabled (at begin of the message)",
+		autolink.Autolink{
+			Pattern:              "(?P<previous>^|\\s)(MM)(-)(?P<jira_id>\\d+)",
+			Template:             "${previous}[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+			DisableNonWordPrefix: true,
+			DisableNonWordSuffix: true,
+		},
+		"MM-12345 should link!",
+		"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
+	}, {
+		"Pattern word prefix and suffix enable (in the middle of other text)",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"WelcomeMM-12345should not link!",
+		"WelcomeMM-12345should not link!",
+	}, {
+		"Pattern word prefix and suffix disabled (in the middle of other text)",
+		autolink.Autolink{
+			Pattern:              "(MM)(-)(?P<jira_id>\\d+)",
+			Template:             "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+			DisableNonWordPrefix: true,
+			DisableNonWordSuffix: true,
+		},
+		"WelcomeMM-12345should link!",
+		"Welcome[MM-12345](https://mattermost.atlassian.net/browse/MM-12345)should link!",
+	}, {
+		"Not relinking",
+		autolink.Autolink{
+			Pattern:  "(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should not re-link!",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should not re-link!",
+	}, {
+		"Url replacement",
+		autolink.Autolink{
+			Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Welcome https://mattermost.atlassian.net/browse/MM-12345 should link!",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345) should link!",
+	}, {
+		"Url replacement multiple times",
+		autolink.Autolink{
+			Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Welcome https://mattermost.atlassian.net/browse/MM-12345. should link https://mattermost.atlassian.net/browse/MM-12346 !",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345). should link [MM-12346](https://mattermost.atlassian.net/browse/MM-12346) !",
+	}, {
+		"Url replacement multiple times and at beginning",
+		autolink.Autolink{
+			Pattern:  "(https:\\/\\/mattermost.atlassian.net\\/browse\\/)(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"https://mattermost.atlassian.net/browse/MM-12345 https://mattermost.atlassian.net/browse/MM-12345",
+		"[MM-12345](https://mattermost.atlassian.net/browse/MM-12345) [MM-12345](https://mattermost.atlassian.net/browse/MM-12345)",
+	}, {
+		"Url replacement at end",
+		autolink.Autolink{
+			Pattern:  "(https://mattermost.atlassian.net/browse/)(MM)(-)(?P<jira_id>\\d+)",
+			Template: "[MM-$jira_id](https://mattermost.atlassian.net/browse/MM-$jira_id)",
+		},
+		"Welcome https://mattermost.atlassian.net/browse/MM-12345",
+		"Welcome [MM-12345](https://mattermost.atlassian.net/browse/MM-12345)",
+	},
+}
+
+func TestJira(t *testing.T) {
+	testLinks(t, jiraTests...)
+}
diff --git a/server/autolink/lib_productboard_test.go b/server/autolink/lib_productboard_test.go
new file mode 100644
index 00000000..8b2df4b5
--- /dev/null
+++ b/server/autolink/lib_productboard_test.go
@@ -0,0 +1,41 @@
+package autolink_test
+
+import (
+	"testing"
+
+	"github.com/mattermost/mattermost-plugin-autolink/server/autolink"
+)
+
+var productboardLink = autolink.Autolink{
+	Pattern:   `(?P<url>https://mattermost\.productboard\.com/.+)`,
+	Template:  "[ProductBoard link]($url)",
+	WordMatch: true,
+}
+
+var productboardTests = []linkTest{
+	{
+		"Url replacement",
+		productboardLink,
+		"Welcome to https://mattermost.productboard.com/somepage should link!",
+		"Welcome to [ProductBoard link](https://mattermost.productboard.com/somepage) should link!",
+	}, {
+		"Not relinking",
+		productboardLink,
+		"Welcome to [other link](https://mattermost.productboard.com/somepage) should not re-link!",
+		"Welcome to [other link](https://mattermost.productboard.com/somepage) should not re-link!",
+	}, {
+		"Word boundary happy",
+		productboardLink,
+		"Welcome to (https://mattermost.productboard.com/somepage) should link!",
+		"Welcome to ([ProductBoard link](https://mattermost.productboard.com/somepage)) should link!",
+	}, {
+		"Word boundary un-happy",
+		productboardLink,
+		"Welcome to (BADhttps://mattermost.productboard.com/somepage) should not link!",
+		"Welcome to (BADhttps://mattermost.productboard.com/somepage) should not link!",
+	},
+}
+
+func TestProductBoard(t *testing.T) {
+	testLinks(t, productboardTests...)
+}
diff --git a/server/autolink/lib_social_security_number_test.go b/server/autolink/lib_social_security_number_test.go
new file mode 100644
index 00000000..06cdc12f
--- /dev/null
+++ b/server/autolink/lib_social_security_number_test.go
@@ -0,0 +1,41 @@
+package autolink_test
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	reSSN      = `(?P<SSN>(?P<part1>\d{3})[ -]?(?P<part2>\d{2})[ -]?(?P<LastFour>[0-9]{4}))`
+	replaceSSN = `XXX-XX-$LastFour`
+)
+
+func TestSocialSecurityNumberRegex(t *testing.T) {
+	for _, tc := range []struct {
+		Name    string
+		RE      string
+		Replace string
+		In      string
+		Out     string
+	}{
+		{"SSN happy spaces", reSSN, replaceSSN, " abc 652 47 3356 def", " abc XXX-XX-3356 def"},
+		{"SSN happy dashes", reSSN, replaceSSN, " abc 652-47-3356 def", " abc XXX-XX-3356 def"},
+		{"SSN happy digits", reSSN, replaceSSN, " abc 652473356 def", " abc XXX-XX-3356 def"},
+		{"SSN happy mixed1", reSSN, replaceSSN, " abc 65247-3356 def", " abc XXX-XX-3356 def"},
+		{"SSN happy mixed2", reSSN, replaceSSN, " abc 652 47-3356 def", " abc XXX-XX-3356 def"},
+		{"SSN non-match 19-09-9999", reSSN, replaceSSN, " abc 19-09-9999 def", " abc 19-09-9999 def"},
+		{"SSN non-match 652_47-3356", reSSN, replaceSSN, " abc 652_47-3356 def", " abc 652_47-3356 def"},
+	} {
+		t.Run(tc.Name, func(t *testing.T) {
+			re := regexp.MustCompile(tc.RE)
+			result := re.ReplaceAllString(tc.In, tc.Replace)
+			if tc.Out != "" {
+				assert.Equal(t, tc.Out, result)
+			} else {
+				assert.Equal(t, tc.In, result)
+			}
+		})
+	}
+}
diff --git a/server/autolinkclient/client.go b/server/autolinkclient/client.go
index 69587a5b..45896c6a 100644
--- a/server/autolinkclient/client.go
+++ b/server/autolinkclient/client.go
@@ -40,7 +40,7 @@ func NewClientPlugin(api PluginAPI) *Client {
 
 func (c *Client) Add(links ...autolink.Autolink) error {
 	for _, link := range links {
-		linkBytes, err := json.Marshal(&link)
+		linkBytes, err := json.Marshal(link)
 		if err != nil {
 			return err
 		}
diff --git a/server/autolinkplugin/command.go b/server/autolinkplugin/command.go
index 6d0e56fb..d9385ac2 100644
--- a/server/autolinkplugin/command.go
+++ b/server/autolinkplugin/command.go
@@ -207,16 +207,20 @@ func executeTest(p *Plugin, c *plugin.Context, header *model.CommandArgs, args .
 	restOfCommand := header.Command[10:] // "/autolink "
 	restOfCommand = restOfCommand[strings.Index(restOfCommand, args[0])+len(args[0]):]
 	orig := strings.TrimSpace(restOfCommand)
-	out := ""
+	out := fmt.Sprintf("- Original: `%s`\n", orig)
 
 	for _, ref := range refs {
 		l := links[ref]
 		l.Disabled = false
+		err = l.Compile()
+		if err != nil {
+			return responsef("failed to compile link %s: %v", l.DisplayName(), err)
+		}
 		replaced := l.Replace(orig)
 		if replaced == orig {
-			out += fmt.Sprintf("- %s: _no change_\n", l.DisplayName())
+			out += fmt.Sprintf("- Link %s: _no change_\n", l.DisplayName())
 		} else {
-			out += fmt.Sprintf("- %s: `%s`\n", l.DisplayName(), replaced)
+			out += fmt.Sprintf("- Link %s: changed to `%s`\n", l.DisplayName(), replaced)
 			orig = replaced
 		}
 	}