Skip to content

Commit

Permalink
Merge pull request #37 from JosephWoodward/scrubber-support
Browse files Browse the repository at this point in the history
Initial support for scrubbers via a fluent API
  • Loading branch information
emilybache authored May 30, 2022
2 parents e60d9ca + 1305c95 commit 32d5677
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 30 deletions.
130 changes: 100 additions & 30 deletions approvals.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"reflect"
"regexp"
"strings"

"github.com/approvals/go-approval-tests/reporters"
Expand All @@ -32,13 +33,49 @@ type Failable interface {
}

// VerifyWithExtension Example:
// VerifyWithExtension(t, strings.NewReader("Hello"), ".txt")
func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string) {
// VerifyWithExtension(t, strings.NewReader("Hello"), ".json")
// Deprecated: Please use Verify with the Options() fluent syntax.
func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string, opts ...verifyOptions) {
t.Helper()
Verify(t, reader, alwaysOption(opts).WithExtension(extWithDot))
}

// Verify Example:
// Verify(t, strings.NewReader("Hello"))
func Verify(t Failable, reader io.Reader, opts ...verifyOptions) {
t.Helper()

if len(opts) > 1 {
panic("Please use fluent syntax for options, see documentation for more information")
}

var extWithDot string
if len(opts) == 0 || opts[0].extWithDot == "" {
extWithDot = ".txt"
} else {
extWithDot = opts[0].extWithDot
}

namer := getApprovalName(t)

if len(opts) > 0 {
b, err := io.ReadAll(reader)
if err != nil {
panic(err)
}

result := string(b)
for _, o := range opts {
for _, sb := range o.scrubbers {
result = sb(result)
}
}

reader = strings.NewReader(result)
}

reporter := getReporter()
var err = namer.compare(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot), reader)
err := namer.compare(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot), reader)
if err != nil {
reporter.Report(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot))
t.Log("Failed Approval: received does not match approved.")
Expand All @@ -48,24 +85,17 @@ func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string) {
}
}

// Verify Example:
// Verify(t, strings.NewReader("Hello"))
func Verify(t Failable, reader io.Reader) {
t.Helper()
VerifyWithExtension(t, reader, ".txt")
}

// VerifyString stores the passed string into the received file and confirms
// that it matches the approved local file. On failure, it will launch a reporter.
func VerifyString(t Failable, s string) {
func VerifyString(t Failable, s string, opts ...verifyOptions) {
t.Helper()
reader := strings.NewReader(s)
Verify(t, reader)
Verify(t, reader, opts...)
}

// VerifyXMLStruct Example:
// VerifyXMLStruct(t, xml)
func VerifyXMLStruct(t Failable, obj interface{}) {
func VerifyXMLStruct(t Failable, obj interface{}, opts ...verifyOptions) {
t.Helper()
xmlContent, err := xml.MarshalIndent(obj, "", " ")
if err != nil {
Expand All @@ -74,15 +104,15 @@ func VerifyXMLStruct(t Failable, obj interface{}) {
tip = "when using anonymous types be sure to include\n XMLName xml.Name `xml:\"Your_Name_Here\"`\n"
}
message := fmt.Sprintf("error while pretty printing XML\n%verror:\n %v\nXML:\n %v\n", tip, err, obj)
VerifyWithExtension(t, strings.NewReader(message), ".xml")
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".xml"))
} else {
VerifyWithExtension(t, bytes.NewReader(xmlContent), ".xml")
Verify(t, bytes.NewReader(xmlContent), alwaysOption(opts).WithExtension(".xml"))
}
}

// VerifyXMLBytes Example:
// VerifyXMLBytes(t, []byte("<Test/>"))
func VerifyXMLBytes(t Failable, bs []byte) {
func VerifyXMLBytes(t Failable, bs []byte, opts ...verifyOptions) {
t.Helper()
type node struct {
Attr []xml.Attr
Expand All @@ -95,65 +125,66 @@ func VerifyXMLBytes(t Failable, bs []byte) {
err := xml.Unmarshal(bs, &x)
if err != nil {
message := fmt.Sprintf("error while parsing XML\nerror:\n %s\nXML:\n %s\n", err, string(bs))
VerifyWithExtension(t, strings.NewReader(message), ".xml")
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".xml"))
} else {
VerifyXMLStruct(t, x)
VerifyXMLStruct(t, x, opts...)
}
}

// VerifyJSONStruct Example:
// VerifyJSONStruct(t, json)
func VerifyJSONStruct(t Failable, obj interface{}) {
func VerifyJSONStruct(t Failable, obj interface{}, opts ...verifyOptions) {
t.Helper()

jsonb, err := json.MarshalIndent(obj, "", " ")
if err != nil {
message := fmt.Sprintf("error while pretty printing JSON\nerror:\n %s\nJSON:\n %s\n", err, obj)
VerifyWithExtension(t, strings.NewReader(message), ".json")
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".json"))
} else {
VerifyWithExtension(t, bytes.NewReader(jsonb), ".json")
Verify(t, bytes.NewReader(jsonb), alwaysOption(opts).WithExtension(".json"))
}
}

// VerifyJSONBytes Example:
// VerifyJSONBytes(t, []byte("{ \"Greeting\": \"Hello\" }"))
func VerifyJSONBytes(t Failable, bs []byte) {
func VerifyJSONBytes(t Failable, bs []byte, opts ...verifyOptions) {
t.Helper()
var obj map[string]interface{}
err := json.Unmarshal(bs, &obj)
if err != nil {
message := fmt.Sprintf("error while parsing JSON\nerror:\n %s\nJSON:\n %s\n", err, string(bs))
VerifyWithExtension(t, strings.NewReader(message), ".json")
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".json"))
} else {
VerifyJSONStruct(t, obj)
VerifyJSONStruct(t, obj, opts...)
}
}

// VerifyMap Example:
// VerifyMap(t, map[string][string] { "dog": "bark" })
func VerifyMap(t Failable, m interface{}) {
func VerifyMap(t Failable, m interface{}, opts ...verifyOptions) {
t.Helper()
outputText := utils.PrintMap(m)
VerifyString(t, outputText)
VerifyString(t, outputText, opts...)
}

// VerifyArray Example:
// VerifyArray(t, []string{"dog", "cat"})
func VerifyArray(t Failable, array interface{}) {
func VerifyArray(t Failable, array interface{}, opts ...verifyOptions) {
t.Helper()
outputText := utils.PrintArray(array)
VerifyString(t, outputText)
VerifyString(t, outputText, opts...)
}

// VerifyAll Example:
// VerifyAll(t, "uppercase", []string("dog", "cat"}, func(x interface{}) string { return strings.ToUpper(x.(string)) })
func VerifyAll(t Failable, header string, collection interface{}, transform func(interface{}) string) {
func VerifyAll(t Failable, header string, collection interface{}, transform func(interface{}) string, opts ...verifyOptions) {
t.Helper()
if len(header) != 0 {
header = fmt.Sprintf("%s\n\n\n", header)
}

outputText := header + strings.Join(utils.MapToString(collection, transform), "\n")
VerifyString(t, outputText)
VerifyString(t, outputText, opts...)
}

type reporterCloser struct {
Expand Down Expand Up @@ -232,3 +263,42 @@ func getReporter() reporters.Reporter {
func UseFolder(f string) {
defaultFolder = f
}

type scrubber func(s string) string

// verifyOptions can be accessed via the approvals.Options() API enabling configuration of scrubbers
type verifyOptions struct {
scrubbers []scrubber
extWithDot string
}

// Options enables providing individual Verify functions with customisations such as scrubbers
func Options() verifyOptions {
return verifyOptions{}
}

// WithRegexScrubber allows you to 'scrub' dynamic data such as timestamps within your test input
// and replace it with a static placeholder
func (v verifyOptions) WithRegexScrubber(scrubber *regexp.Regexp, replacer string) verifyOptions {
v.scrubbers = append(v.scrubbers, func(s string) string {
return scrubber.ReplaceAllString(s, replacer)
})
return v
}

// WithExtension overrides the default file extension (.txt) for approval files.
func (v verifyOptions) WithExtension(extension string) verifyOptions {
v.extWithDot = extension
return v
}

func alwaysOption(opts []verifyOptions) verifyOptions {
var v verifyOptions
if len(opts) == 0 {
v = Options()
} else {
v = opts[0]
}

return v
}
88 changes: 88 additions & 0 deletions scrubber_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package approvals_test

import (
"fmt"
"regexp"
"strings"
"testing"
"time"

approvals "github.com/approvals/go-approval-tests"
)

func TestVerifyDoesNotAcceptSeveralVerifyOptions(t *testing.T) {
scrubber1, _ := regexp.Compile("\\d{10}$")
opts1 := approvals.Options().WithRegexScrubber(scrubber1, "<time>")
opts2 := approvals.Options().WithRegexScrubber(scrubber1, "<time>")

m := strings.NewReader("Hello World")

defer func() { _ = recover() }()

approvals.Verify(t, m, opts1, opts2)
t.Errorf("Panic expected")
}

func TestVerifyMapWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("\\d{10}$")
opts := approvals.Options().WithRegexScrubber(scrubber, "<time>")

m := map[string]string{
"dog": "bark",
"cat": "meow",
"time": fmt.Sprint(time.Now().Unix()),
}
approvals.VerifyMap(t, m, opts)
}

func TestVerifyArrayWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("cat")
opts := approvals.Options().WithRegexScrubber(scrubber, "person")

xs := []string{"dog", "cat", "bird"}
approvals.VerifyArray(t, xs, opts)
}

func TestVerifyJSONBytesWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("Hello")
opts := approvals.Options().WithRegexScrubber(scrubber, "Hi")

jb := []byte("{ \"Greeting\": \"Hello\" }")
approvals.VerifyJSONBytes(t, jb, opts)
}

func TestVerifyXMLBytesWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("Hello")
opts := approvals.Options().WithRegexScrubber(scrubber, "Hi")

xmlb := []byte("<Test><Title>Hello World!</Title><Name>Peter Pan</Name><Age>100</Age></Test>")
approvals.VerifyXMLBytes(t, xmlb, opts)
}

func TestVerifyStringWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("\\d{10}$")
opts := approvals.Options().WithRegexScrubber(scrubber, "<now>")

s := fmt.Sprintf("The time is %v", time.Now().Unix())
approvals.VerifyString(t, s, opts)
}

func TestVerifyStringWithMultipleScrubbers(t *testing.T) {
scrubber1, _ := regexp.Compile("\\d{10}$")
scrubber2, _ := regexp.Compile("time")

opts := approvals.Options().
WithRegexScrubber(scrubber1, "<now>").
WithRegexScrubber(scrubber2, "<future>")

s := fmt.Sprintf("The time is %v", time.Now().Unix())
approvals.VerifyString(t, s, opts)
}

func TestVerifyAllWithRegexScrubber(t *testing.T) {
scrubber, _ := regexp.Compile("Llewellyn")
opts := approvals.Options().WithRegexScrubber(scrubber, "Walken")

xs := []string{"Christopher", "Llewellyn"}
approvals.VerifyAll(t, "uppercase", xs, func(x interface{}) string { return fmt.Sprintf("%s => %s", x, strings.ToUpper(x.(string))) }, opts)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
uppercase


Christopher => CHRISTOPHER
Walken => LLEWELLYN
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[0]=dog
[1]=person
[2]=bird
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Greeting": "Hi"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[cat]=meow
[dog]=bark
[time]=<time>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The <future> is <now>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The time is <now>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Test>
<Title>Hi World!</Title>
<Name>Peter Pan</Name>
<Age>100</Age>
</Test>

0 comments on commit 32d5677

Please sign in to comment.