diff --git a/README.md b/README.md index c5083a3..d6bb9b4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Only works for JSON API ## Dependencies ```bash -"github.com/bastianrob/go-valuator" +"github.com/robertkrimen/otto" "github.com/buger/jsonparser" ``` diff --git a/case.go b/case.go index 6d3aaf5..55133d2 100644 --- a/case.go +++ b/case.go @@ -3,7 +3,7 @@ package restify import ( "regexp" - "github.com/bastianrob/go-restify/enum" + "github.com/SpaceStock/go-restify/enum" ) var ( @@ -12,17 +12,17 @@ var ( //Pipeline test pipeline as what to do with the response object type Pipeline struct { - Cache bool `json:"cache" bson:"cache"` - CacheAs string `json:"cache_as" bson:"cache_as"` - OnFailure enum.OnFailure `json:"on_failure" bson:"on_failure"` + Cache bool `json:"cache"` + CacheAs string `json:"cache_as"` + OnFailure enum.OnFailure `json:"on_failure"` } //TestCase struct type TestCase struct { - Order uint `json:"order" bson:"order"` - Name string `json:"name" bson:"name"` - Description string `json:"description" bson:"description"` - Request Request `json:"request" bson:"request"` - Expect Expect `json:"expect" bson:"expect"` - Pipeline Pipeline `json:"pipeline" bson:"pipeline"` + Order uint `json:"order"` + Name string `json:"name"` + Description string `json:"description"` + Request Request `json:"request"` + Expect Expect `json:"expect"` + Pipeline Pipeline `json:"pipeline"` } diff --git a/enum/onfailure/enum.go b/enum/onfailure/enum.go index 5b5558a..d4f83e3 100644 --- a/enum/onfailure/enum.go +++ b/enum/onfailure/enum.go @@ -1,6 +1,6 @@ package onfailure -import "github.com/bastianrob/go-restify/enum" +import "github.com/SpaceStock/go-restify/enum" //Enumeration constants const ( diff --git a/expect.go b/expect.go index fb6059c..a27a07b 100644 --- a/expect.go +++ b/expect.go @@ -1,35 +1,29 @@ package restify -import ( - "encoding/json" - "strings" - - "github.com/buger/jsonparser" -) - //Expect response expectation type Expect struct { - StatusCode int `json:"status_code" bson:"status_code"` - Headers map[string]string `json:"headers" bson:"headers"` - Evaluate []Expression `json:"evaluate" bson:"evaluate"` + StatusCode int `json:"status_code"` + Headers map[string]string `json:"headers"` + Evaluate []Expression `json:"evaluate"` } -//Parse cache into evaluation value -//This will replace {....} with existing value in cache -func (e *Expect) Parse(cache map[string]json.RawMessage) { - for i, exp := range e.Evaluate { - matches := replacable.FindAllStringSubmatch(exp.Value, -1) - for _, match := range matches { - param := match[0] - keys := strings.Split(match[1], ".") - cacheKey := keys[0] - cacheProps := keys[1:] +// TODO: NO LONGER NEEDED +// Parse cache into evaluation value +// This will replace {....} with existing value in cache +// func (e *Expect) Parse(cache map[string]json.RawMessage) { +// for i, exp := range e.Evaluate { +// matches := replacable.FindAllStringSubmatch(exp.Value, -1) +// for _, match := range matches { +// param := match[0] +// keys := strings.Split(match[1], ".") +// cacheKey := keys[0] +// cacheProps := keys[1:] - obj := cache[cacheKey] - strval, _ := jsonparser.GetString(obj, cacheProps...) +// obj := cache[cacheKey] +// strval, _ := jsonparser.GetString(obj, cacheProps...) - exp.Value = strings.Replace(exp.Value, param, strval, 1) - e.Evaluate[i] = exp - } - } -} +// exp.Value = strings.Replace(exp.Value, param, strval, 1) +// e.Evaluate[i] = exp +// } +// } +// } diff --git a/expression.go b/expression.go index 8d72955..4e2913b 100644 --- a/expression.go +++ b/expression.go @@ -1,10 +1,31 @@ package restify +import ( + "github.com/robertkrimen/otto" +) + //Expression rule of expected response -type Expression struct { - Object string `json:"object" bson:"object"` - Prop string `json:"prop" bson:"prop"` - Operator string `json:"operator" bson:"operator"` - Value string `json:"value" bson:"value"` - Description string `json:"description" bson:"description"` +type Expression string + +// IsTrue evaluate given boolean expression whether it is true, or false +// Return false on invalid expression +func (expr Expression) IsTrue(input map[string]interface{}) bool { + jsvm := otto.New() + for key, val := range input { + jsvm.Set(key, val) + } + + // jsvm.Set("res", input) + // jsvm.Run("res = JSON.parse(res)") + val, err := jsvm.Run(string(expr)) + if err != nil { + return false + } + + result, err := val.ToBoolean() + if err != nil { + return false + } + + return result } diff --git a/expression_test.go b/expression_test.go new file mode 100644 index 0000000..f4d62a6 --- /dev/null +++ b/expression_test.go @@ -0,0 +1,49 @@ +package restify + +import ( + "encoding/json" + "testing" +) + +func TestExpression_IsTrue(t *testing.T) { + type args struct { + json string + pair map[string]interface{} + } + tests := []struct { + name string + expr Expression + args args + want bool + }{{ + name: "Simple positive case 1", + args: args{json: `{"name": "Mr. Brother"}`}, //We receive this from API call + expr: Expression(`name === "Mr. Brother"`), //We want to test the response with this expression + want: true, //Given such response, against the expr rule, we want the expression result to be true + }, { + name: "Simple positive case 2", + args: args{json: `{"age": 10}`}, + expr: Expression(`age >= 10`), + want: true, + }, { + name: "Composite positive case 3", + args: args{json: `{"name": "John", "age": 10}`}, + expr: Expression(`name === "John" && age >= 10`), + want: true, + }, { + name: "Complex object positive case 3", + args: args{json: `{"person": {"name": "John", "age": 10} }`}, + expr: Expression(`person.name === "John" && person.age >= 10`), + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pair := map[string]interface{}{} + json.Unmarshal([]byte(tt.args.json), &pair) + + if got := tt.expr.IsTrue(pair); got != tt.want { + t.Errorf("Expression.IsTrue() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/request.go b/request.go index dd85b06..1ae320a 100644 --- a/request.go +++ b/request.go @@ -10,10 +10,10 @@ import ( //Request test object type Request struct { - URL string `json:"url" bson:"url"` - Method string `json:"method" bson:"method"` - Headers map[string]string `json:"headers" bson:"headers"` - Payload map[string]interface{} `json:"payload" bson:"payload"` + URL string `json:"url"` + Method string `json:"method"` + Headers map[string]string `json:"headers"` + Payload map[string]interface{} `json:"payload"` } func recursiveMapParser(obj map[string]interface{}, cache map[string]json.RawMessage) map[string]interface{} { diff --git a/result.go b/result.go index 8293f23..f08e3af 100644 --- a/result.go +++ b/result.go @@ -6,24 +6,25 @@ import "time" //Hopefully de-normalizing the structure can make analytic easier //Generally immutable, so we don't need ID and just let DB auto gen for us type TestResult struct { - Timestamp int64 `json:"timestamp" bson:"timestamp"` - ScenarioName string `json:"scenario_name" bson:"scenario_name"` - TestCaseOrder int `json:"test_case_order" bson:"test_case_order"` - TestCaseName string `json:"test_case_name" bson:"test_case_name"` - RequestMethod string `json:"request_method" bson:"request_method"` - RequestURL string `json:"request_url" bson:"request_url"` + Timestamp int64 `json:"timestamp"` + ScenarioName string `json:"scenario_name"` + TestCaseOrder int `json:"test_case_order"` + TestCaseName string `json:"test_case_name"` + RequestMethod string `json:"request_method"` + RequestURL string `json:"request_url"` //RequestPayload? - ResponseCode int `json:"response_code" bson:"response_code"` - ResponseSize int64 `json:"response_size" bson:"response_size"` + ResponseCode int `json:"response_code"` + ResponseSize int64 `json:"response_size"` //Response timing - TimingDNS time.Duration `json:"timing_dns" bson:"timing_dns"` - TimingHandshake time.Duration `json:"timing_handshake" bson:"timing_handshake"` - TimingConnected time.Duration `json:"timing_connected" bson:"timing_connected"` - TimingFirstByte time.Duration `json:"timing_first_byte" bson:"timing_first_byte"` - TimingTotal time.Duration `json:"timing_total" bson:"timing_total"` + TimingDNS time.Duration `json:"timing_dns"` + TimingHandshake time.Duration `json:"timing_handshake"` + TimingConnected time.Duration `json:"timing_connected"` + TimingFirstByte time.Duration `json:"timing_first_byte"` + TimingTotal time.Duration `json:"timing_total"` //ResponsePayload? - ExpectedCode int `json:"expected_code" bson:"expected_code"` - Message string `json:"message" bson:"message"` + Success bool `json:"success"` + ExpectedCode int `json:"expected_code"` + Message string `json:"message"` } //NewTestResult from scenario diff --git a/scenario/scenario.go b/scenario/scenario.go index b673933..7f13e78 100644 --- a/scenario/scenario.go +++ b/scenario/scenario.go @@ -9,15 +9,10 @@ import ( "io/ioutil" "net/http" "net/http/httptrace" - "strings" "time" - "github.com/buger/jsonparser" - "go.mongodb.org/mongo-driver/bson" - - restify "github.com/bastianrob/go-restify" - "github.com/bastianrob/go-restify/enum/onfailure" - valuator "github.com/bastianrob/go-valuator" + restify "github.com/SpaceStock/go-restify" + "github.com/SpaceStock/go-restify/enum/onfailure" ) //implementation of restify.Scenario @@ -94,44 +89,6 @@ func (s *scenario) UnmarshalJSON(data []byte) error { return nil } -func (s *scenario) MarshalBSON() ([]byte, error) { - return bson.Marshal(struct { - ID string `bson:"_id"` - Name string `bson:"name"` - Description string `bson:"description"` - Environment string `bson:"environment"` - Cases []restify.TestCase `bson:"cases"` - }{ - ID: s.id, - Name: s.name, - Description: s.description, - Environment: s.environment, - Cases: s.cases, - }) -} - -func (s *scenario) UnmarshalBSON(data []byte) error { - alias := struct { - ID string `bson:"_id"` - Name string `bson:"name"` - Description string `bson:"description"` - Environment string `bson:"environment"` - Cases []restify.TestCase `bson:"cases"` - }{} - - err := bson.Unmarshal(data, &alias) - if err != nil { - return err - } - - s.id = alias.ID - s.name = alias.Name - s.description = alias.Description - s.environment = alias.Environment - s.cases = alias.Cases - return nil -} - //TODO: UGLY AF CODE! TOO EFFIN FAT! NEED TO REFACTOR! func (s *scenario) Run(w io.Writer) []restify.TestResult { io.WriteString(w, fmt.Sprintf( @@ -140,6 +97,8 @@ func (s *scenario) Run(w io.Writer) []restify.TestResult { testResults := []restify.TestResult{} httpClient := http.Client{} + +loop: for i, tc := range s.cases { io.WriteString(w, fmt.Sprintf( "%d. Test case: name=%s desc=%s onfail=%s\r\n", @@ -149,7 +108,7 @@ func (s *scenario) Run(w io.Writer) []restify.TestResult { //Parse any cache needed tc.Request.Parse(s.cache) - tc.Expect.Parse(s.cache) + // tc.Expect.Parse(s.cache) payload, _ := json.Marshal(tc.Request.Payload) //Setup HTTP request @@ -271,68 +230,36 @@ func (s *scenario) Run(w io.Writer) []restify.TestResult { continue } - //evaluate every rule - valid := true - invalidMsg := "" - for ri, rule := range tc.Expect.Evaluate { - eval, err := valuator.NewValuator(rule.Prop, rule.Operator, rule.Value, rule.Description) - if err != nil && tc.Pipeline.OnFailure == onfailure.Exit { - msg := fmt.Sprintf("%d.%d. Failed to get evaluator: %s\r\n", (i + 1), (ri + 1), err.Error()) + // TODO: Evaluate every rule + var pair map[string]interface{} + json.Unmarshal(body, &pair) // convert []byte to map[string]interface{} + + for _, expr := range tc.Expect.Evaluate { // foreach rule in evaluate + isValid := expr.IsTrue(pair) + if !isValid && tc.Pipeline.OnFailure == onfailure.Exit { + msg := fmt.Sprintf("%d. Expression Failed : Status %t\r\n", (i + 1), isValid) io.WriteString(w, msg) tr.Message = msg testResults = append(testResults, tr) + return testResults - } else if err != nil { - msg := fmt.Sprintf("%d.%d. Failed to get evaluator: %s\r\n", (i + 1), (ri + 1), err.Error()) + } else if !isValid { + msg := fmt.Sprintf("%d. Expression Failed : Status %t\r\n", (i + 1), isValid) io.WriteString(w, msg) tr.Message = msg testResults = append(testResults, tr) - continue - } - obj := make(map[string]interface{}) - { //parse response body into map and take the evaluation object - if rule.Object == "" { - err = json.Unmarshal(body, &obj) - } else { - paths := strings.Split(rule.Object, ".") - val, _, _, _ := jsonparser.Get(body, paths...) - err = json.Unmarshal(val, &obj) - } - - if err != nil && tc.Pipeline.OnFailure == onfailure.Exit { - msg := fmt.Sprintf("%d. Failed to parse response body into map: %s\r\n", (i + 1), err.Error()) - io.WriteString(w, msg) - - tr.Message = msg - testResults = append(testResults, tr) - return testResults - } else if err != nil { - msg := fmt.Sprintf("%d. Failed to parse response body into map: %s\r\n", (i + 1), err.Error()) - io.WriteString(w, msg) - - tr.Message = msg - testResults = append(testResults, tr) - continue - } - } + continue loop + } else { + msg := fmt.Sprintf("%d. Expression Success: Status %t\r\n", (i + 1), isValid) + io.WriteString(w, msg) - valid = eval.Evaluate(obj) - if !valid { - invalidMsg = fmt.Sprintf("%d.%d. Expectation failed against rule=%+v\r\n", (i + 1), (ri + 1), rule) - io.WriteString(w, invalidMsg) - break + continue } } - if !valid && tc.Pipeline.OnFailure == onfailure.Exit { - tr.Message = invalidMsg - testResults = append(testResults, tr) - return testResults - } - //cache if needed if tc.Pipeline.Cache { s.cache[tc.Pipeline.CacheAs] = body @@ -341,6 +268,7 @@ func (s *scenario) Run(w io.Writer) []restify.TestResult { msg := fmt.Sprintf("%d. Success\r\n", (i + 1)) io.WriteString(w, msg) + tr.Success = true tr.Message = msg testResults = append(testResults, tr) } diff --git a/scenario/scenario_getter.go b/scenario/scenario_getter.go index 29a77f5..10841e4 100644 --- a/scenario/scenario_getter.go +++ b/scenario/scenario_getter.go @@ -1,6 +1,6 @@ package scenario -import restify "github.com/bastianrob/go-restify" +import restify "github.com/SpaceStock/go-restify" type getter struct { scenario *scenario diff --git a/scenario/scenario_setter.go b/scenario/scenario_setter.go index 359e104..8aec38c 100644 --- a/scenario/scenario_setter.go +++ b/scenario/scenario_setter.go @@ -3,7 +3,7 @@ package scenario import ( "sort" - restify "github.com/bastianrob/go-restify" + restify "github.com/SpaceStock/go-restify" ) type setter struct { diff --git a/scenario_test/scenario_test.go b/scenario_test/scenario_test.go new file mode 100644 index 0000000..efeb4f8 --- /dev/null +++ b/scenario_test/scenario_test.go @@ -0,0 +1,355 @@ +package restify + +import ( + "os" + "testing" + + restify "github.com/SpaceStock/go-restify" + "github.com/SpaceStock/go-restify/enum/onfailure" + "github.com/SpaceStock/go-restify/scenario" + "github.com/stretchr/testify/assert" +) + +func Test_Scenario(t *testing.T) { + scn := scenario.New() + results := scn. + Set().ID("").Name("Complex Testing"). + AddCase(restify.TestCase{ + Order: 1, + Name: "Firebase Auth", + Description: "", + Request: restify.Request{ + URL: "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyD-HHHsWb82AFmdXtm0t86Nb9uoMJutrU0", + Method: "POST", + Payload: map[string]interface{}{ + "email": "superadmin@spacestock.com", + "password": "admin@123", + "returnSecureToken": true, + }, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "idToken != ''", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "auth", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 2, + Name: "Get Complex Apartment", + Description: "", + Request: restify.Request{ + URL: "https://stg-satpam.spacestock.com/1.0/complex/apartment?page=1&size=1", + Method: "GET", + Headers: map[string]string{ + "Authorization": "Bearer {auth.idToken}", + }, + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{}, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "list", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 3, + Name: "Get One Apartment", + Description: "", + Request: restify.Request{ + URL: "https://stg-satpam.spacestock.com/1.0/complex/apartment/{list.data.[0].id}", + Method: "GET", + Headers: map[string]string{ + "Authorization": "Bearer {auth.idToken}", + }, + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "id === '{list.data.[0.id]}'", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "oneApt", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 4, + Name: "Create Apartment", + Description: "", + Request: restify.Request{ + URL: "https://stg-satpam.spacestock.com/1.0/complex/apartment", + Method: "POST", + Headers: map[string]string{ + "Authorization": "Bearer {auth.idToken}", + }, + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 201, + Evaluate: []restify.Expression{}, + }, + Pipeline: restify.Pipeline{ + Cache: false, + CacheAs: "aptOne", + OnFailure: onfailure.Exit, + }, + }).End(). + Run(os.Stdout) + + if len(results) <= 0 { + t.Error("No result returned") + } + + if results[0].Success { + t.Error("This case should have failed") + } +} + +// Data True +func Test_Scenario2(t *testing.T) { + scn := scenario.New() + results := scn. + Set().ID("").Name("Scenario 2"). + AddCase(restify.TestCase{ + Order: 1, + Name: "Test Case 1", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/1", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 1", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc1", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 2, + Name: "Test Case 2", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/2", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 2", // False + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc2", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 3, + Name: "Test Case 3", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/3", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 3", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc1", + OnFailure: onfailure.Exit, + }, + }).End(). + Run(os.Stdout) + + assert.NotEqual(t, 0, len(results), "Seharusnya bukan 0") + assert.True(t, results[0].Success) + assert.True(t, results[1].Success) +} + +// Pipeline.OnFailure=Exit +func Test_Scenario3(t *testing.T) { + scn := scenario.New() + results := scn. + Set().ID("").Name("Scenario 2"). + AddCase(restify.TestCase{ + Order: 1, + Name: "Test Case 1", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/1", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 1", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc1", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 2, + Name: "Test Case 2", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/2", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 3", // False + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc2", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 3, + Name: "Test Case 3", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/3", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 3", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc3", + OnFailure: onfailure.Exit, + }, + }).End(). + Run(os.Stdout) + + assert.Equal(t, 2, len(results), "Test Case = 2") + assert.True(t, results[0].Success) + assert.False(t, results[1].Success) +} + +// Pipeline.OnFailure=Fallthrough +func Test_Scenario4(t *testing.T) { + scn := scenario.New() + results := scn. + Set().ID("").Name("Scenario 3"). + AddCase(restify.TestCase{ + Order: 1, + Name: "Test Case 1", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/1", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 1", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc1", + OnFailure: onfailure.Exit, + }, + }). + AddCase(restify.TestCase{ + Order: 2, + Name: "Test Case 2", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/2", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 1", // False + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc2", + OnFailure: onfailure.Fallthrough, + }, + }). + AddCase(restify.TestCase{ + Order: 3, + Name: "Test Case 3", + Description: "", + Request: restify.Request{ + URL: "http://jsonplaceholder.typicode.com/posts/3", + Method: "GET", + Payload: nil, + }, + Expect: restify.Expect{ + StatusCode: 200, + Evaluate: []restify.Expression{ + "userId && userId === 1", + "id && id === 3", + }, + }, + Pipeline: restify.Pipeline{ + Cache: true, + CacheAs: "tc3", + OnFailure: onfailure.Exit, + }, + }).End(). + Run(os.Stdout) + + assert.Equal(t, 3, len(results), "Test Case = 3") + + assert.True(t, results[0].Success) + assert.False(t, results[1].Success) + assert.True(t, results[2].Success) +}