From a4bd054b28a70d275c6b8bbed4f1cde82f210f7a Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 17:55:18 +0300 Subject: [PATCH 1/7] improve readme --- EXAMPLES.md | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 226 ++++++--------------------------------- 2 files changed, 335 insertions(+), 194 deletions(-) create mode 100644 EXAMPLES.md diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..84dc849 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,303 @@ +# Examples + +Подробные примеры использования пакетов по темам. + +## Содержание + +- [apitest](#apitest) + - [GET-запрос и проверка JSON](#apitest---get-запрос-и-проверка-json) +- [assertjson](#assertjson) + - [Базовые проверки](#assertjson---базовые-проверки) + - [Строки](#assertjson---строки) + - [Числа](#assertjson---числа) + - [UUID, email, URL](#assertjson---uuid-email-url) + - [Время и даты](#assertjson---время-и-даты) + - [Массивы и объекты](#assertjson---массивы-и-объекты) + - [Пути и переиспользуемые проверки](#assertjson---пути-и-переиспользуемые-проверки) + - [JWT](#assertjson---jwt) + - [Получение значений и отладка](#assertjson---получение-значений-и-отладка) +- [assertxml](#assertxml) + - [Базовые проверки XML](#assertxml---базовые-проверки-xml) + +--- + +## apitest + +### apitest — GET-запрос и проверка JSON + +```go +package yours + +import ( + "net/http" + "testing" + + "github.com/muonsoft/api-testing/apitest" + "github.com/muonsoft/api-testing/assertjson" +) + +func TestYourAPI(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ok":true}`)) + }) + + response := apitest.HandleGET(t, handler, "/example") + + response.IsOK() + response.HasContentType("application/json") + response.HasJSON(func(json *assertjson.AssertJSON) { + json.Node("ok").IsTrue() + }) + response.Print() + response.PrintJSON() +} +``` + +--- + +## assertjson + +Примеры предполагают, что JSON загружен из `recorder.Body.Bytes()` или файла через `assertjson.Has(t, data, ...)` / `assertjson.FileHas(t, filename, ...)`. + +### assertjson — Базовые проверки + +```go +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("nullNode").Exists() + json.Node("notExistingNode").DoesNotExist() + json.Node("nullNode").IsNull() + json.Node("stringNode").IsNotNull() + json.Node("trueBooleanNode").IsTrue() + json.Node("falseBooleanNode").IsFalse() + json.Node("objectNode").EqualJSON(`{"objectKey": "objectValue"}`) +}) +``` + +### assertjson — Строки + +```go +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("stringNode").IsString() + json.Node("stringNode").Matches("^string.*$") + json.Node("stringNode").DoesNotMatch("^notMatch$") + json.Node("stringNode").Contains("string") + json.Node("stringNode").DoesNotContain("notContain") + + // fluent-цепочки + json.Node("emptyString").IsString().IsEmpty() + json.Node("stringNode").IsString().IsNotEmpty() + json.Node("stringNode").IsString().EqualTo("stringValue") + json.Node("stringNode").IsString().EqualToOneOf("stringValue", "nextValue") + json.Node("stringNode").IsString().NotEqualTo("invalid") + json.Node("stringNode").IsString().WithLength(11) + json.Node("stringNode").IsString().WithLengthGreaterThan(10) + json.Node("stringNode").IsString().WithLengthLessThan(12) + json.Node("stringNode").IsString().That(func(s string) error { + if s != "stringValue" { + return fmt.Errorf("invalid") + } + return nil + }) +}) +``` + +### assertjson — Числа + +```go +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("integerNode").IsInteger() + json.Node("zeroInteger").IsInteger().IsZero() + json.Node("integerNode").IsInteger().IsNotZero() + json.Node("integerNode").IsInteger().EqualTo(123) + json.Node("integerNode").IsInteger().GreaterThan(122) + json.Node("integerNode").IsInteger().LessThanOrEqual(123) + + json.Node("floatNode").IsFloat() + json.Node("floatNode").IsNumber() + json.Node("floatNode").IsNumber().EqualTo(123.123) + json.Node("floatNode").IsNumber().EqualToWithDelta(123.123, 0.1) + json.Node("floatNode").IsNumber().GreaterThanOrEqual(122).LessThanOrEqual(124) +}) +``` + +### assertjson — UUID, email, URL + +```go +import "github.com/gofrs/uuid/v5" + +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("uuid").IsString().WithUUID() + json.Node("uuid").IsUUID().IsNotNil().OfVersion(4).OfVariant(1) + json.Node("uuid").IsUUID().EqualTo(uuid.FromStringOrNil("23e98a0c-26c8-410f-978f-d1d67228af87")) + json.Node("nilUUID").IsUUID().IsNil() + + json.Node("email").IsEmail() + json.Node("email").IsHTML5Email() + json.Node("url").IsURL().WithSchemas("https").WithHosts("example.com") +}) +``` + +### assertjson — Время и даты + +```go +import "time" + +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("time").IsTime().EqualTo(time.Date(2022, time.October, 16, 12, 14, 32, 0, time.UTC)) + json.Node("time").IsTime().After(time.Date(2021, time.October, 16, 12, 14, 32, 0, time.UTC)) + json.Node("time").IsTime().Before(time.Date(2023, time.October, 16, 12, 14, 32, 0, time.UTC)) + json.Node("time").IsTime().AtDate(2022, time.October, 16) + + json.Node("date").IsDate().EqualToDate(2022, time.October, 16) + json.Node("date").IsDate().AfterDate(2021, time.October, 16) + json.Node("date").IsDate().BeforeOrEqualToDate(2022, time.October, 16) +}) +``` + +### assertjson — Массивы и объекты + +```go +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + json.Node("arrayNode").IsArray() + json.Node("arrayNode").IsArray().WithLength(1) + json.Node("arrayNode").IsArray().WithLengthGreaterThan(0) + json.Node("arrayNode").IsArray().WithUniqueElements() + json.Node("arrayNode").ForEach(func(node *assertjson.AssertNode) { + node.IsString().EqualTo("arrayValue") + }) + + json.Node("objectNode").IsObject() + json.Node("objectNode").IsObject().WithPropertiesCount(1) + json.Node("objectNode").IsObject().WithPropertiesCountGreaterThan(0) + json.Node("objectNode").ForEach(func(node *assertjson.AssertNode) { + node.IsString().EqualTo("objectValue") + }) +}) +``` + +### assertjson — Пути и переиспользуемые проверки + +```go +import "github.com/gofrs/uuid/v5" + +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + // путь по элементам + json.Node("bookstore", "books", 1, "name").IsString().EqualTo("Green book") + + // fmt.Stringer в пути + id := uuid.FromStringOrNil("9b1100ea-986b-446b-ae7e-0c8ce7196c26") + json.Node("hashmap", id, "key").IsString().EqualTo("value") + + // сложные ключи (JSON-LD, Hydra и т.п.) + json.Node("@id").IsString().EqualTo("json-ld-id") + json.Node("hydra:members").IsString().EqualTo("hydraMembers") + + // переиспользуемая проверка + isGreenBook := func(json *assertjson.AssertJSON) { + json.Node("id").IsInteger().EqualTo(123) + json.Node("name").IsString().EqualTo("Green book") + } + json.Node("bookstore", "books", 1).Assert(isGreenBook) + json.Node("bookstore", "bestBook").Assert(isGreenBook) + isGreenBook(json.At("bookstore", "books", 1)) + isGreenBook(json.At("bookstore", "bestBook")) +}) +``` + +### assertjson — JWT + +```go +import ( + "time" + "github.com/golang-jwt/jwt/v5" +) + +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + isJWT := json.Node("jwt").IsJWT(func(token *jwt.Token) (interface{}, error) { + return []byte("your-256-bit-secret"), nil + }) + isJWT. + WithAlgorithm("HS256"). + WithID("abc12345"). + WithIssuer("https://issuer.example.com"). + WithSubject("https://subject.example.com"). + WithAudience([]string{"https://audience1.example.com", "https://audience2.example.com"}). + WithHeader(func(json *assertjson.AssertJSON) { + json.Node("alg").IsString().EqualTo("HS256") + json.Node("typ").IsString().EqualTo("JWT") + }). + WithPayload(func(json *assertjson.AssertJSON) { + json.Node("name").IsString().EqualTo("John Doe") + }) + isJWT.WithExpiresAt().AfterDate(2022, time.October, 26) + isJWT.WithNotBefore().BeforeDate(2022, time.October, 27) + isJWT.WithIssuedAt().BeforeDate(2022, time.October, 27) +}) + +// Проверка JWT из строки (без контекста JSON) +assertjson.IsJWT(t, rawJWTString, keyFunc).WithPayload(func(json *assertjson.AssertJSON) { + json.Node("name").IsString().EqualTo("John Doe") +}) +``` + +### assertjson — Получение значений и отладка + +```go +import ( + "time" + "github.com/stretchr/testify/assert" +) + +assertjson.Has(t, data, func(json *assertjson.AssertJSON) { + _ = json.Node("stringNode").Value() + _ = json.Node("stringNode").String() + _ = json.Node("integerNode").Integer() + _ = json.Node("integerNode").Float() + _ = json.Node("floatNode").Float() + _ = json.Node("arrayNode").IsArray().Length() + _ = json.Node("objectNode").IsObject().PropertiesCount() + _ = json.Node("objectNode").JSON() + _ = json.Node("time").Time().Format(time.RFC3339) + _ = json.Node("uuid").IsUUID().Value().String() + + assert.Equal(t, "stringValue", json.Node("stringNode").String()) + assert.Equal(t, 123, json.Node("integerNode").Integer()) + + // вывод узла в тестовый лог (отладка) + json.Node("bookstore", "books", 1).Print() +}) +``` + +--- + +## assertxml + +### assertxml — Базовые проверки XML + +```go +package yours + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/muonsoft/api-testing/assertxml" +) + +func TestYourAPI(t *testing.T) { + recorder := httptest.NewRecorder() + handler := createHTTPHandler() + + request, _ := http.NewRequest("GET", "/content", nil) + handler.ServeHTTP(recorder, request) + + assertxml.Has(t, recorder.Body.Bytes(), func(xml *assertxml.AssertXML) { + xml.Node("/root/stringNode").Exists() + xml.Node("/root/notExistingNode").DoesNotExist() + xml.Node("/root/stringNode").EqualToTheString("stringValue") + }) +} +``` diff --git a/README.md b/README.md index 75c1052..d3cedd1 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,28 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/muonsoft/api-testing)](https://goreportcard.com/report/github.com/muonsoft/api-testing) ![CI](https://github.com/muonsoft/api-testing/workflows/CI/badge.svg?branch=master) +## Contents + +- [Installation](#installation) +- [apitest package](#apitest-package) — HTTP handler testing +- [assertjson package](#assertjson-package) — JSON assertions +- [assertxml package](#assertxml-package) — XML assertions +- [Examples](EXAMPLES.md) — подробные примеры по темам + +## Installation + +```bash +go get github.com/muonsoft/api-testing +``` + +--- + ## `apitest` package The `apitest` package provides methods for testing client-server communication. -It can be used to test `http.Handler` to build complex assertions on the HTTP responses. +It can be used to test `http.Handler` and build assertions on HTTP responses (status, headers, body). -Example +### Example ```go package yours @@ -44,25 +60,22 @@ func TestYourAPI(t *testing.T) { } ``` +--- + ## `assertjson` package -The `assertjson` package provides methods for testing JSON values. Selecting JSON values provided by [JSON Pointer Syntax](https://tools.ietf.org/html/rfc6901). +The `assertjson` package provides fluent assertions for JSON values. Nodes are selected via [JSON Pointer](https://tools.ietf.org/html/rfc6901) or path elements. Supports strings, numbers, arrays, objects, UUID, email, URL, time, and JWT. -Example +### Example ```go package yours import ( - "fmt" "net/http/httptest" "testing" - "time" - "github.com/gofrs/uuid/v5" - "github.com/golang-jwt/jwt/v5" "github.com/muonsoft/api-testing/assertjson" - "github.com/stretchr/testify/assert" ) func TestYourAPI(t *testing.T) { @@ -73,202 +86,26 @@ func TestYourAPI(t *testing.T) { handler.ServeHTTP(recorder, request) assertjson.Has(t, recorder.Body.Bytes(), func(json *assertjson.AssertJSON) { - // common assertions json.Node("nullNode").Exists() json.Node("notExistingNode").DoesNotExist() - json.Node("nullNode").IsNull() - json.Node("stringNode").IsNotNull() - json.Node("trueBooleanNode").IsTrue() - json.Node("falseBooleanNode").IsFalse() - json.Node("objectNode").EqualJSON(`{"objectKey": "objectValue"}`) - - // string assertions - json.Node("stringNode").IsString() - json.Node("stringNode").Matches("^string.*$") - json.Node("stringNode").DoesNotMatch("^notMatch$") - json.Node("stringNode").Contains("string") - json.Node("stringNode").DoesNotContain("notContain") - - // fluent string assertions - json.Node("stringNode").IsString() - json.Node("emptyString").IsString().IsEmpty() - json.Node("stringNode").IsString().IsNotEmpty() json.Node("stringNode").IsString().EqualTo("stringValue") - json.Node("stringNode").IsString().EqualToOneOf("stringValue", "nextValue") - json.Node("stringNode").IsString().NotEqualTo("invalid") - json.Node("stringNode").IsString().Matches("^string.*$") - json.Node("stringNode").IsString().NotMatches("^notMatch$") - json.Node("stringNode").IsString().Contains("string") - json.Node("stringNode").IsString().NotContains("notContain") - json.Node("stringNode").IsString().WithLength(11) - json.Node("stringNode").IsString().WithLengthGreaterThan(10) - json.Node("stringNode").IsString().WithLengthGreaterThanOrEqual(11) - json.Node("stringNode").IsString().WithLengthLessThan(12) - json.Node("stringNode").IsString().WithLengthLessThanOrEqual(11) - json.Node("stringNode").IsString().That(func(s string) error { - if s != "stringValue" { - return fmt.Errorf("invalid") - } - return nil - }) - json.Node("stringNode").IsString().Assert(func(t testing.TB, value string) { - assert.Equal(t, "stringValue", value) - }) - - // numeric assertions - json.Node("integerNode").IsInteger() - json.Node("zeroInteger").IsInteger().IsZero() - json.Node("integerNode").IsInteger().IsNotZero() json.Node("integerNode").IsInteger().EqualTo(123) - json.Node("integerNode").IsInteger().NotEqualTo(321) - json.Node("integerNode").IsInteger().GreaterThan(122) - json.Node("integerNode").IsInteger().GreaterThanOrEqual(123) - json.Node("integerNode").IsInteger().LessThan(124) - json.Node("integerNode").IsInteger().LessThanOrEqual(123) - json.Node("floatNode").IsFloat() - json.Node("floatNode").IsNumber() - json.Node("zeroFloat").IsNumber().IsZero() - json.Node("floatNode").IsNumber().IsNotZero() - json.Node("floatNode").IsNumber().EqualTo(123.123) - json.Node("floatNode").IsNumber().NotEqualTo(321.123) - json.Node("floatNode").IsNumber().EqualToWithDelta(123.123, 0.1) - json.Node("floatNode").IsNumber().GreaterThan(122) - json.Node("floatNode").IsNumber().GreaterThanOrEqual(123.123) - json.Node("floatNode").IsNumber().LessThan(124) - json.Node("floatNode").IsNumber().LessThanOrEqual(123.123) - json.Node("floatNode").IsNumber().GreaterThanOrEqual(122).LessThanOrEqual(124) - - // string values assertions - json.Node("uuid").IsString().WithUUID() - json.Node("uuid").IsUUID().IsNotNil().OfVersion(4).OfVariant(1) - json.Node("uuid").IsUUID().EqualTo(uuid.FromStringOrNil("23e98a0c-26c8-410f-978f-d1d67228af87")) - json.Node("uuid").IsUUID().NotEqualTo(uuid.FromStringOrNil("a54cbd42-b30c-4619-b89a-47375734d49c")) - json.Node("nilUUID").IsUUID().IsNil() - json.Node("email").IsEmail() - json.Node("email").IsHTML5Email() - json.Node("url").IsURL().WithSchemas("https").WithHosts("example.com") - - // time assertions - json.Node("time").IsTime().EqualTo(time.Date(2022, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().NotEqualTo(time.Date(2021, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().AfterOrEqualTo(time.Date(2022, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().After(time.Date(2021, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().Before(time.Date(2023, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().BeforeOrEqualTo(time.Date(2022, time.October, 16, 12, 14, 32, 0, time.UTC)) - json.Node("time").IsTime().AtDate(2022, time.October, 16) - json.Node("date").IsDate().EqualToDate(2022, time.October, 16) - json.Node("date").IsDate().NotEqualToDate(2021, time.October, 16) - json.Node("date").IsDate().AfterDate(2021, time.October, 16) - json.Node("date").IsDate().AfterOrEqualToDate(2022, time.October, 16) - json.Node("date").IsDate().BeforeDate(2023, time.October, 16) - json.Node("date").IsDate().BeforeOrEqualToDate(2022, time.October, 16) - - // array assertions - json.Node("arrayNode").IsArray() - json.Node("arrayNode").IsArray().WithLength(1) - json.Node("arrayNode").IsArray().WithLengthGreaterThan(0) - json.Node("arrayNode").IsArray().WithLengthGreaterThanOrEqual(1) - json.Node("arrayNode").IsArray().WithLengthLessThan(2) - json.Node("arrayNode").IsArray().WithLengthLessThanOrEqual(1) - json.Node("arrayNode").IsArray().WithUniqueElements() - json.Node("arrayNode").ForEach(func(node *assertjson.AssertNode) { - node.IsString().EqualTo("arrayValue") - }) - - // object assertions - json.Node("objectNode").IsObject() - json.Node("objectNode").IsObject().WithPropertiesCount(1) - json.Node("objectNode").IsObject().WithPropertiesCountGreaterThan(0) - json.Node("objectNode").IsObject().WithPropertiesCountGreaterThanOrEqual(1) - json.Node("objectNode").IsObject().WithPropertiesCountLessThan(2) - json.Node("objectNode").IsObject().WithPropertiesCountLessThanOrEqual(1) - json.Node("objectNode").IsObject().WithUniqueElements() - json.Node("objectNode").ForEach(func(node *assertjson.AssertNode) { - node.IsString().EqualTo("objectValue") - }) - - // seek node by path elements + json.Node("objectNode").EqualJSON(`{"objectKey": "objectValue"}`) json.Node("bookstore", "books", 1, "name").IsString().EqualTo("Green book") - - // use fmt.Stringer in node path - id := uuid.FromStringOrNil("9b1100ea-986b-446b-ae7e-0c8ce7196c26") - json.Node("hashmap", id, "key").IsString().EqualTo("value") - - // complex keys - json.Node("@id").IsString().EqualTo("json-ld-id") - json.Node("hydra:members").IsString().EqualTo("hydraMembers") - - // reusable assertions - isGreenBook := func(json *assertjson.AssertJSON) { - json.Node("id").IsInteger().EqualTo(123) - json.Node("name").IsString().EqualTo("Green book") - } - json.Node("bookstore", "books", 1).Assert(isGreenBook) - json.Node("bookstore", "bestBook").Assert(isGreenBook) - isGreenBook(json.At("bookstore", "books", 1)) - isGreenBook(json.At("bookstore", "bestBook")) - - // JSON Web Token (JWT) assertion - isJWT := json.Node("jwt").IsJWT(func(token *jwt.Token) (interface{}, error) { - return []byte("your-256-bit-secret"), nil - }) - isJWT. - WithAlgorithm("HS256"). - // standard claims assertions - WithID("abc12345"). - WithIssuer("https://issuer.example.com"). - WithSubject("https://subject.example.com"). - WithAudience([]string{"https://audience1.example.com", "https://audience2.example.com"}). - // json assertion of header part - WithHeader(func(json *assertjson.AssertJSON) { - json.Node("alg").IsString().EqualTo("HS256") - json.Node("typ").IsString().EqualTo("JWT") - }). - // json assertion of payload part - WithPayload(func(json *assertjson.AssertJSON) { - json.Node("name").IsString().EqualTo("John Doe") - }) - // time assertions for standard claims - isJWT.WithExpiresAt().AfterDate(2022, time.October, 26) - isJWT.WithNotBefore().BeforeDate(2022, time.October, 27) - isJWT.WithIssuedAt().BeforeDate(2022, time.October, 27) - - // get node values - assert.Equal(t, "stringValue", json.Node("stringNode").Value()) - assert.Equal(t, "stringValue", json.Node("stringNode").String()) - assert.Equal(t, "123", json.Node("integerNode").String()) - assert.Equal(t, "123.123000", json.Node("floatNode").String()) - assert.Equal(t, 123.0, json.Node("integerNode").Float()) - assert.Equal(t, 123.123, json.Node("floatNode").Float()) - assert.Equal(t, 123, json.Node("integerNode").Integer()) - assert.Equal(t, 1, json.Node("arrayNode").IsArray().Length()) - assert.Equal(t, 1, json.Node("arrayNode").ArrayLength()) - assert.Equal(t, 1, json.Node("objectNode").IsObject().PropertiesCount()) - assert.Equal(t, 1, json.Node("objectNode").ObjectPropertiesCount()) - assert.JSONEq(t, `{"objectKey": "objectValue"}`, string(json.Node("objectNode").JSON())) - assert.Equal(t, "2022-10-16T15:14:32+03:00", json.Node("time").Time().Format(time.RFC3339)) - assert.Equal(t, "23e98a0c-26c8-410f-978f-d1d67228af87", json.Node("uuid").IsUUID().Value().String()) - assert.Equal(t, "23e98a0c-26c8-410f-978f-d1d67228af87", json.Node("uuid").UUID().String()) - - // standalone JWT assertion - assertjson.IsJWT(t, - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hdWRpZW5jZTEuZXhhbXBsZS5jb20iLCJodHRwczovL2F1ZGllbmNlMi5leGFtcGxlLmNvbSJdLCJleHAiOjQ4MjAzNjAxMzEsImlhdCI6MTY2Njc1NjUzMSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJqdGkiOiJhYmMxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsIm5iZiI6MTY2Njc1NjUzMSwic3ViIjoiaHR0cHM6Ly9zdWJqZWN0LmV4YW1wbGUuY29tIn0.fGUvIn-BV8bPKkZdrxUneew3_qBe-knptL9a_TkNA4M", - func(token *jwt.Token) (interface{}, error) { return []byte("your-256-bit-secret"), nil }, - ).WithPayload(func(json *assertjson.AssertJSON) { - json.Node("name").IsString().EqualTo("John Doe") - }) - - // debug helpers - json.Node("bookstore", "books", 1).Print() + json.Node("bookstore", "books", 1).Print() // debug helper }) } ``` +Подробные примеры по темам (строки, числа, массивы, объекты, UUID, время, JWT и др.) — в [EXAMPLES.md](EXAMPLES.md). Полный API — [pkg.go.dev/github.com/muonsoft/api-testing/assertjson](https://pkg.go.dev/github.com/muonsoft/api-testing/assertjson). + +--- + ## `assertxml` package -The `assertjson` package provides methods for testing XML values. Selecting XML values provided by XML Path Syntax. +The `assertxml` package provides methods for testing XML values. Nodes are selected via XML Path syntax. -Example +### Example ```go package yours @@ -277,6 +114,7 @@ import ( "net/http" "net/http/httptest" "testing" + "github.com/muonsoft/api-testing/assertxml" ) @@ -287,7 +125,7 @@ func TestYourAPI(t *testing.T) { request, _ := http.NewRequest("GET", "/content", nil) handler.ServeHTTP(recorder, request) - assertxml.Has(t, recorder.Body.Bytes(), func(xml *AssertXML) { + assertxml.Has(t, recorder.Body.Bytes(), func(xml *assertxml.AssertXML) { // common assertions xml.Node("/root/stringNode").Exists() xml.Node("/root/notExistingNode").DoesNotExist() From 2fef3c9f121387cf220ced6ba3a8dee567b1f972 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 18:01:59 +0300 Subject: [PATCH 2/7] update linter to v2.6 --- .github/workflows/main.yml | 2 +- .golangci.yml | 109 ++++++++++++++++++++----------------- assertions/time.go | 2 +- assertjson/array.go | 1 + assertjson/assertjson.go | 2 + assertjson/identifiers.go | 4 ++ assertjson/numeric.go | 7 +++ assertjson/object.go | 1 + assertjson/string.go | 4 ++ assertjson/time.go | 2 +- 10 files changed, 81 insertions(+), 53 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3d31ef..6f3d830 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.63 + version: v2.61 - name: Run tests run: go test -v $(go list ./... | grep -v vendor) diff --git a/.golangci.yml b/.golangci.yml index 47fd335..919bd7a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,4 @@ +version: "2" linters: enable: - asciicheck @@ -8,85 +9,93 @@ linters: - depguard - dogsled - durationcheck - - errcheck - errorlint - forbidigo - funlen - - gci - gocognit - goconst - gocritic - gocyclo - godot - godox - - gofmt - - gofumpt - - goimports - gomodguard - goprintffuncname - gosec - - gosimple - - govet - importas - - ineffassign - makezero - misspell - nakedret - nestif - nilerr - noctx - - noctx - nolintlint - prealloc - predeclared - promlinter - rowserrcheck - sqlclosecheck - - stylecheck - - tenv + - staticcheck - testpackage - thelper - tparallel - - typecheck - unconvert - unparam - - unused - whitespace - -issues: - exclude-dirs: - - var - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - funlen - - lll - - gocyclo - - errcheck - - dupl - - gosec - - scopelint - - gochecknoglobals - - goerr113 - -linters-settings: - depguard: + settings: + depguard: + rules: + main: + files: + - $all + - '!$test' + - '!**/test/**/*' + allow: + - $gostd + - github.com + - gopkg.in + test: + files: + - $test + allow: + - $gostd + - github.com + gosec: + excludes: + - G115 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - main: - files: - - $all - - "!$test" - - "!**/test/**/*" - allow: - - $gostd - - github.com - test: - files: - - "$test" - allow: - - $gostd - - github.com - gosec: - excludes: - - G115 + - linters: + - dupl + - err113 + - errcheck + - funlen + - gochecknoglobals + - gocyclo + - gosec + - lll + - scopelint + path: _test\.go + paths: + - var + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - var + - third_party$ + - builtin$ + - examples$ diff --git a/assertions/time.go b/assertions/time.go index 382fccf..f86a0a2 100644 --- a/assertions/time.go +++ b/assertions/time.go @@ -216,7 +216,7 @@ func (a *TimeAssertion) AtDate(year int, month time.Month, day int, msgAndArgs . begin := newDate(year, month, day) end := begin.Add(24 * time.Hour) - if !((a.value.After(begin) || a.value.Equal(begin)) && a.value.Before(end)) { + if (!a.value.After(begin) && !a.value.Equal(begin)) || !a.value.Before(end) { a.fail( fmt.Sprintf( `at date "%s", actual is "%s"`, diff --git a/assertjson/array.go b/assertjson/array.go index 384fdde..0f6dfb4 100644 --- a/assertjson/array.go +++ b/assertjson/array.go @@ -10,6 +10,7 @@ import ( ) // IsArrayWithElementsCount asserts that the JSON node is an array with given elements count. +// // Deprecated: use IsArray().WithLength() instead. func (node *AssertNode) IsArrayWithElementsCount(count int, msgAndArgs ...interface{}) { node.t.Helper() diff --git a/assertjson/assertjson.go b/assertjson/assertjson.go index 389ad9a..06e5bc0 100644 --- a/assertjson/assertjson.go +++ b/assertjson/assertjson.go @@ -80,6 +80,7 @@ func (j *AssertJSON) Node(path ...interface{}) *AssertNode { // Nodef searches for JSON node by JSON Path Syntax. Returns struct for asserting the node values. // It calculates path by applying fmt.Sprintf function. +// // Deprecated: use Node() with multiple arguments. func (j *AssertJSON) Nodef(format string, a ...interface{}) *AssertNode { j.t.Helper() @@ -108,6 +109,7 @@ func (j *AssertJSON) At(path ...interface{}) *AssertJSON { // Atf is used to test assertions on some node in a batch. It returns AssertJSON object on that node. // It calculates path by applying fmt.Sprintf function. +// // Deprecated: use At() with multiple arguments. func (j *AssertJSON) Atf(format string, a ...interface{}) *AssertJSON { j.t.Helper() diff --git a/assertjson/identifiers.go b/assertjson/identifiers.go index 6380c41..774cb6a 100644 --- a/assertjson/identifiers.go +++ b/assertjson/identifiers.go @@ -157,24 +157,28 @@ func (node *AssertNode) UUID() uuid.UUID { } // Nil asserts that the JSON node has a string value equals to nil UUID. +// // Deprecated: use IsNil(). func (a *UUIDAssertion) Nil(msgAndArgs ...interface{}) *UUIDAssertion { return a.IsNil(msgAndArgs...) } // NotNil asserts that the JSON node has a string value equals to not nil UUID. +// // Deprecated: use IsNotNil(). func (a *UUIDAssertion) NotNil(msgAndArgs ...interface{}) *UUIDAssertion { return a.IsNotNil(msgAndArgs...) } // Version asserts that the JSON node has a string value equals to UUID with the given version. +// // Deprecated: use OfVersion(). func (a *UUIDAssertion) Version(version byte, msgAndArgs ...interface{}) *UUIDAssertion { return a.OfVersion(version, msgAndArgs...) } // Variant asserts that the JSON node has a string value equals to UUID with the given variant. +// // Deprecated: use OfVariant(). func (a *UUIDAssertion) Variant(variant byte, msgAndArgs ...interface{}) *UUIDAssertion { return a.OfVariant(variant, msgAndArgs...) diff --git a/assertjson/numeric.go b/assertjson/numeric.go index 8075b71..9e654ac 100644 --- a/assertjson/numeric.go +++ b/assertjson/numeric.go @@ -69,6 +69,7 @@ func (node *AssertNode) IsNumber(msgAndArgs ...interface{}) *NumberAssertion { } // EqualToTheInteger asserts that the JSON node has an integer value equals to the given value. +// // Deprecated: use IsInteger() instead. func (node *AssertNode) EqualToTheInteger(expectedValue int, msgAndArgs ...interface{}) { node.t.Helper() @@ -76,6 +77,7 @@ func (node *AssertNode) EqualToTheInteger(expectedValue int, msgAndArgs ...inter } // EqualToTheFloat asserts that the JSON node has a float value equals to the given value. +// // Deprecated: use IsNumber() instead. func (node *AssertNode) EqualToTheFloat(expectedValue float64, msgAndArgs ...interface{}) { node.t.Helper() @@ -83,6 +85,7 @@ func (node *AssertNode) EqualToTheFloat(expectedValue float64, msgAndArgs ...int } // IsNumberGreaterThan asserts that the JSON node has a number greater than the given value. +// // Deprecated: use IsNumber().GreaterThan() instead. func (node *AssertNode) IsNumberGreaterThan(value float64, msgAndArgs ...interface{}) { node.t.Helper() @@ -90,6 +93,7 @@ func (node *AssertNode) IsNumberGreaterThan(value float64, msgAndArgs ...interfa } // IsNumberGreaterThanOrEqual asserts that the JSON node has a number greater than or equal to the given value. +// // Deprecated: use IsNumber().GreaterThanOrEqual() instead. func (node *AssertNode) IsNumberGreaterThanOrEqual(value float64, msgAndArgs ...interface{}) { node.t.Helper() @@ -97,6 +101,7 @@ func (node *AssertNode) IsNumberGreaterThanOrEqual(value float64, msgAndArgs ... } // IsNumberLessThan asserts that the JSON node has a number less than the given value. +// // Deprecated: use IsNumber().LessThan() instead. func (node *AssertNode) IsNumberLessThan(value float64, msgAndArgs ...interface{}) { node.t.Helper() @@ -104,6 +109,7 @@ func (node *AssertNode) IsNumberLessThan(value float64, msgAndArgs ...interface{ } // IsNumberLessThanOrEqual asserts that the JSON node has a number less than or equal to the given value. +// // Deprecated: use IsNumber().LessThanOrEqual() instead. func (node *AssertNode) IsNumberLessThanOrEqual(value float64, msgAndArgs ...interface{}) { node.t.Helper() @@ -111,6 +117,7 @@ func (node *AssertNode) IsNumberLessThanOrEqual(value float64, msgAndArgs ...int } // IsNumberInRange asserts that the JSON node has a number with value in the given range. +// // Deprecated: use IsNumber().GreaterThanOrEqual().LessThanOrEqual() instead. func (node *AssertNode) IsNumberInRange(vmin, vmax float64, msgAndArgs ...interface{}) { node.t.Helper() diff --git a/assertjson/object.go b/assertjson/object.go index aaccefd..0dd9486 100644 --- a/assertjson/object.go +++ b/assertjson/object.go @@ -10,6 +10,7 @@ import ( ) // IsObjectWithPropertiesCount asserts that the JSON node is an object with given properties count. +// // Deprecated: use IsObject().WithPropertiesCount() instead. func (node *AssertNode) IsObjectWithPropertiesCount(count int, msgAndArgs ...interface{}) { node.t.Helper() diff --git a/assertjson/string.go b/assertjson/string.go index eb54965..577135d 100644 --- a/assertjson/string.go +++ b/assertjson/string.go @@ -34,6 +34,7 @@ func (node *AssertNode) IsString(msgAndArgs ...interface{}) *StringAssertion { } // EqualToTheString asserts that the JSON node has a string value equals to the given value. +// // Deprecated: use IsString().EqualTo() instead. func (node *AssertNode) EqualToTheString(expectedValue string, msgAndArgs ...interface{}) { node.t.Helper() @@ -65,6 +66,7 @@ func (node *AssertNode) DoesNotContain(contain string, msgAndArgs ...interface{} } // IsStringWithLength asserts that the JSON node has a string value with length equal to the given value. +// // Deprecated: use IsString().WithLength() instead. func (node *AssertNode) IsStringWithLength(length int, msgAndArgs ...interface{}) { node.t.Helper() @@ -72,6 +74,7 @@ func (node *AssertNode) IsStringWithLength(length int, msgAndArgs ...interface{} } // IsStringWithLengthInRange asserts that the JSON node has a string value with length in a given range. +// // Deprecated: use IsString().WithLengthGreaterThanOrEqual().WithLengthLessThanOrEqual() instead. func (node *AssertNode) IsStringWithLengthInRange(vmin int, vmax int, msgAndArgs ...interface{}) { node.t.Helper() @@ -79,6 +82,7 @@ func (node *AssertNode) IsStringWithLengthInRange(vmin int, vmax int, msgAndArgs } // AssertString asserts that the JSON node has a string value and it is satisfied by the user function assertFunc. +// // Deprecated: use IsString().Assert() instead. func (node *AssertNode) AssertString(assertFunc func(t testing.TB, value string)) { node.t.Helper() diff --git a/assertjson/time.go b/assertjson/time.go index ebb7981..dc0cada 100644 --- a/assertjson/time.go +++ b/assertjson/time.go @@ -235,7 +235,7 @@ func (a *TimeAssertion) AtDate(year int, month time.Month, day int, msgAndArgs . begin := newDate(year, month, day) end := begin.Add(24 * time.Hour) - if !((a.value.After(begin) || a.value.Equal(begin)) && a.value.Before(end)) { + if (!a.value.After(begin) && !a.value.Equal(begin)) || !a.value.Before(end) { a.fail( fmt.Sprintf( `is time at date "%s", actual is "%s"`, From 3d0cb46d203329835c1504174cdca3df00c8ee85 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 18:02:50 +0300 Subject: [PATCH 3/7] fix: correct linter version to v2.6.1 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6f3d830..1759deb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v2.61 + version: v2.6.1 - name: Run tests run: go test -v $(go list ./... | grep -v vendor) From f93787d92c399493eb6d2e536991796922a67ca2 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 18:05:24 +0300 Subject: [PATCH 4/7] update: upgrade GitHub Actions and Go setup versions, improve test command --- .github/workflows/main.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1759deb..441d981 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,22 +9,23 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: ^1.16 + cache-dependency-path: go.sum id: go - - name: Checkout code - uses: actions/checkout@v2 - - name: Set up dependencies run: go mod download - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v9 with: version: v2.6.1 - name: Run tests - run: go test -v $(go list ./... | grep -v vendor) + run: go test -race -v ./... From 5682c54e3c48a8777a4ca1857a80cd3bacf0e207 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 18:28:38 +0300 Subject: [PATCH 5/7] feat: add support for JSON Lines (NDJSON) assertions - Introduced `LinesHas` and `FileLinesHas` functions for asserting multiple JSON objects in NDJSON format. - Updated documentation in EXAMPLES.md and README.md to include usage examples for JSON Lines. - Added unit tests for new functionality in lines_test.go. - Implemented parsing logic for JSON Lines in lines.go. --- EXAMPLES.md | 27 ++++++++ README.md | 11 +++ assertjson/lines.go | 143 +++++++++++++++++++++++++++++++++++++++ assertjson/lines_test.go | 121 +++++++++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 assertjson/lines.go create mode 100644 assertjson/lines_test.go diff --git a/EXAMPLES.md b/EXAMPLES.md index 84dc849..563687f 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -15,6 +15,7 @@ - [Массивы и объекты](#assertjson---массивы-и-объекты) - [Пути и переиспользуемые проверки](#assertjson---пути-и-переиспользуемые-проверки) - [JWT](#assertjson---jwt) + - [JSON Lines (NDJSON)](#assertjson---json-lines-ndjson) - [Получение значений и отладка](#assertjson---получение-значений-и-отладка) - [assertxml](#assertxml) - [Базовые проверки XML](#assertxml---базовые-проверки-xml) @@ -242,6 +243,32 @@ assertjson.IsJWT(t, rawJWTString, keyFunc).WithPayload(func(json *assertjson.Ass }) ``` +### assertjson — JSON Lines (NDJSON) + +Данные в формате JSON Lines (одна строка JSON на строку текста) проверяются через `LinesHas` или `Lines(t, data).Has(...)`. Пустые строки пропускаются. + +```go +// Пакетная функция (аналог Has для одного JSON) +assertjson.LinesHas(t, body, func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("id").EqualToTheInteger(1) + lines.At(0).Node("name").EqualToTheString("Alice") + lines.At(1).Node("id").EqualToTheInteger(2) + lines.At(1).Node("role").EqualToTheString("admin") + lines.WithLength(2) +}) + +// Метод на результате Lines +assertjson.Lines(t, body).Has(func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("event").EqualToTheString("created") + lines.At(1).Node("event").EqualToTheString("updated") +}) + +// Файл +assertjson.FileLinesHas(t, "events.ndjson", func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("event").EqualToTheString("created") +}) +``` + ### assertjson — Получение значений и отладка ```go diff --git a/README.md b/README.md index d3cedd1..aa7fc66 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,17 @@ func TestYourAPI(t *testing.T) { } ``` +**JSON Lines (NDJSON):** use `LinesHas` or `Lines(t, data).Has(...)` to assert over multiple JSON objects, one per line: + +```go +assertjson.LinesHas(t, body, func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("id").EqualToTheInteger(1) + lines.At(0).Node("name").EqualToTheString("Alice") + lines.At(1).Node("role").EqualToTheString("admin") + lines.WithLength(2) +}) +``` + Подробные примеры по темам (строки, числа, массивы, объекты, UUID, время, JWT и др.) — в [EXAMPLES.md](EXAMPLES.md). Полный API — [pkg.go.dev/github.com/muonsoft/api-testing/assertjson](https://pkg.go.dev/github.com/muonsoft/api-testing/assertjson). --- diff --git a/assertjson/lines.go b/assertjson/lines.go new file mode 100644 index 0000000..d8107b9 --- /dev/null +++ b/assertjson/lines.go @@ -0,0 +1,143 @@ +package assertjson + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + + "github.com/muonsoft/api-testing/internal/js" + "github.com/stretchr/testify/assert" +) + +// AssertJSONLines holds parsed JSON Lines for assertion. +type AssertJSONLines struct { + t TestingT + message string + lines []interface{} +} + +// LinesAssertFunc is a callback function used for asserting JSON Lines. +type LinesAssertFunc func(lines *AssertJSONLines) + +// LinesHas parses data as JSON Lines and runs the callback for asserting lines. +// Returns false if t has already failed. +func LinesHas(t TestingT, data []byte, linesAssert LinesAssertFunc) bool { + t.Helper() + lines := parseLines(t, data) + if lines == nil { + return false + } + body := &AssertJSONLines{t: t, lines: lines} + linesAssert(body) + return !t.Failed() +} + +// FileLinesHas reads a file as JSON Lines and runs the callback for asserting lines. +func FileLinesHas(t TestingT, filename string, linesAssert LinesAssertFunc) bool { + t.Helper() + + data, err := os.ReadFile(filename) + if err != nil { + assert.Fail(t, fmt.Sprintf(`failed to read file "%s": %s`, filename, err.Error())) + return false + } + + return LinesHas(t, data, linesAssert) +} + +// Lines parses data as JSON Lines and returns AssertJSONLines for further use. +// Use Has() to run assertions in a callback, or call At(), Len(), WithLength() directly. +func Lines(t TestingT, data []byte) *AssertJSONLines { + t.Helper() + lines := parseLines(t, data) + if lines == nil { + return &AssertJSONLines{t: t, lines: []interface{}{}} + } + return &AssertJSONLines{t: t, lines: lines} +} + +// FileLines reads a file as JSON Lines and returns AssertJSONLines. +func FileLines(t TestingT, filename string) *AssertJSONLines { + t.Helper() + + data, err := os.ReadFile(filename) + if err != nil { + assert.Fail(t, fmt.Sprintf(`failed to read file "%s": %s`, filename, err.Error())) + return &AssertJSONLines{t: t, lines: []interface{}{}} + } + + return Lines(t, data) +} + +// Has runs the callback with this AssertJSONLines. Returns false if t has already failed. +func (l *AssertJSONLines) Has(linesAssert LinesAssertFunc) bool { + l.t.Helper() + linesAssert(l) + return !l.t.Failed() +} + +// At returns AssertJSON for the line at the given index (0-based). +// Fails the test if index is out of range. +func (l *AssertJSONLines) At(index int) *AssertJSON { + l.t.Helper() + if index < 0 || index >= len(l.lines) { + l.fail(fmt.Sprintf( + `JSON Lines index %d is out of range (lines count: %d)`, + index, + len(l.lines), + )) + return &AssertJSON{t: l.t, path: js.NewPath(js.ArrayIndex(index)), data: nil} + } + return &AssertJSON{ + t: l.t, + message: l.message, + path: js.NewPath(js.ArrayIndex(index)), + data: l.lines[index], + } +} + +// Len returns the number of parsed lines. +func (l *AssertJSONLines) Len() int { + l.t.Helper() + return len(l.lines) +} + +// WithLength asserts that the number of lines equals expected. Returns l for chaining. +func (l *AssertJSONLines) WithLength(expected int) *AssertJSONLines { + l.t.Helper() + if len(l.lines) != expected { + l.fail(fmt.Sprintf( + `JSON Lines count is %d, actual is %d`, + expected, + len(l.lines), + )) + } + return l +} + +func (l *AssertJSONLines) fail(message string, msgAndArgs ...interface{}) { + l.t.Helper() + assert.Fail(l.t, l.message+message, msgAndArgs...) +} + +func parseLines(t TestingT, data []byte) []interface{} { + t.Helper() + rawLines := bytes.Split(data, []byte("\n")) + lines := make([]interface{}, 0, len(rawLines)) + lineNum := 0 + for _, line := range rawLines { + lineNum++ + line = bytes.TrimSuffix(bytes.TrimSpace(line), []byte("\r")) + if len(line) == 0 { + continue + } + var value interface{} + if err := json.Unmarshal(line, &value); err != nil { + assert.Fail(t, fmt.Sprintf("JSON Lines line %d: %s", lineNum, err.Error())) + return nil + } + lines = append(lines, value) + } + return lines +} diff --git a/assertjson/lines_test.go b/assertjson/lines_test.go new file mode 100644 index 0000000..69b1f2b --- /dev/null +++ b/assertjson/lines_test.go @@ -0,0 +1,121 @@ +package assertjson_test + +import ( + "testing" + + "github.com/muonsoft/api-testing/assertjson" + "github.com/muonsoft/api-testing/internal/mock" + "github.com/stretchr/testify/assert" +) + +func TestLinesHas_ValidLines(t *testing.T) { + data := []byte(`{"key": "a", "id": 1} +{"other": "value", "id": 2} +`) + + ok := assertjson.LinesHas(t, data, func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("key").Exists() + lines.At(0).Node("key").IsString().EqualTo("a") + lines.At(0).Node("id").IsInteger().EqualTo(1) + lines.At(1).Node("other").IsString().EqualTo("value") + lines.At(1).Node("id").IsInteger().EqualTo(2) + lines.WithLength(2) + }) + assert.True(t, ok) +} + +func TestLinesHas_EmptyLinesSkipped(t *testing.T) { + data := []byte(`{"key": "first"} + +{"key": "second"} +`) + + ok := assertjson.LinesHas(t, data, func(lines *assertjson.AssertJSONLines) { + lines.At(0).Node("key").IsString().EqualTo("first") + lines.At(1).Node("key").IsString().EqualTo("second") + lines.WithLength(2) + }) + assert.True(t, ok) +} + +func TestLinesHas_WithLengthMismatch(t *testing.T) { + data := []byte(`{"a": 1} +{"b": 2} +`) + + tester := &mock.Tester{} + assertjson.LinesHas(tester, data, func(lines *assertjson.AssertJSONLines) { + lines.WithLength(3) + }) + assert.True(t, tester.Failed()) +} + +func TestLinesHas_AtOutOfRangeNegative(t *testing.T) { + data := []byte(`{"a": 1} +`) + + tester := &mock.Tester{} + assertjson.LinesHas(tester, data, func(lines *assertjson.AssertJSONLines) { + lines.At(-1).Node("a").Exists() + }) + assert.True(t, tester.Failed()) +} + +func TestLinesHas_AtOutOfRangeBeyondLen(t *testing.T) { + data := []byte(`{"a": 1} +`) + + tester := &mock.Tester{} + assertjson.LinesHas(tester, data, func(lines *assertjson.AssertJSONLines) { + lines.At(99).Node("a").Exists() + }) + assert.True(t, tester.Failed()) +} + +func TestLinesHas_InvalidLineInMiddle(t *testing.T) { + data := []byte(`{"a": 1} +not json +{"b": 2} +`) + + tester := &mock.Tester{} + ok := assertjson.LinesHas(tester, data, func(lines *assertjson.AssertJSONLines) { + lines.WithLength(3) + }) + assert.False(t, ok) + assert.True(t, tester.Failed()) +} + +func TestLines_MethodHas(t *testing.T) { + data := []byte(`{"event": "created"} +{"event": "updated"} +`) + + lines := assertjson.Lines(t, data) + ok := lines.Has(func(l *assertjson.AssertJSONLines) { + l.At(0).Node("event").IsString().EqualTo("created") + l.At(1).Node("event").IsString().EqualTo("updated") + l.WithLength(2) + }) + assert.True(t, ok) +} + +func TestLines_AtAndLenDirectly(t *testing.T) { + data := []byte(`{"id": 1} +{"id": 2} +`) + + lines := assertjson.Lines(t, data) + assert.Equal(t, 2, lines.Len()) + lines.At(0).Node("id").IsInteger().EqualTo(1) + lines.At(1).Node("id").IsInteger().EqualTo(2) +} + +func TestFileLinesHas_FileNotFound(t *testing.T) { + tester := &mock.Tester{} + ok := assertjson.FileLinesHas(tester, "./nonexistent.ndjson", func(lines *assertjson.AssertJSONLines) { + lines.WithLength(0) + }) + assert.False(t, ok) + assert.True(t, tester.Failed()) +} From 9dee5b9c2e4b0618c8725cada7c5219d09dfff1b Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 19:19:13 +0300 Subject: [PATCH 6/7] feat: enhance string assertions with prefix, suffix, integer, and number checks - Added `WithPrefix` and `WithSuffix` methods to assert JSON string values against specified prefixes and suffixes. - Introduced `WithInteger` and `WithNumber` methods to validate that JSON string values represent integers and numbers, respectively. - Updated EXAMPLES.md to include new assertion examples. - Expanded unit tests in assertjson_test.go to cover new assertion functionalities. --- EXAMPLES.md | 4 + assertjson/assertjson_test.go | 140 ++++++++++++++++++++++++++++++++++ assertjson/string.go | 85 +++++++++++++++++++++ 3 files changed, 229 insertions(+) diff --git a/EXAMPLES.md b/EXAMPLES.md index 563687f..31c779c 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -95,6 +95,10 @@ assertjson.Has(t, data, func(json *assertjson.AssertJSON) { json.Node("stringNode").IsString().WithLength(11) json.Node("stringNode").IsString().WithLengthGreaterThan(10) json.Node("stringNode").IsString().WithLengthLessThan(12) + json.Node("stringNode").IsString().WithPrefix("string") + json.Node("stringNode").IsString().WithSuffix("Value") + json.Node("idNode").IsString().WithInteger().EqualTo(42) + json.Node("amountNode").IsString().WithNumber().EqualTo(12.5) json.Node("stringNode").IsString().That(func(s string) error { if s != "stringValue" { return fmt.Errorf("invalid") diff --git a/assertjson/assertjson_test.go b/assertjson/assertjson_test.go index 99eff2f..9de3b74 100644 --- a/assertjson/assertjson_test.go +++ b/assertjson/assertjson_test.go @@ -1057,6 +1057,146 @@ func TestHas(t *testing.T) { `failed asserting that JSON node "key": not contains "alu", actual is "value"`, }, }, + { + name: "JSON node is string with prefix", + json: `{"key": "value"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithPrefix("val") + }, + }, + { + name: "JSON node is string with prefix fails", + json: `{"key": "value"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithPrefix("foo") + }, + wantMessages: []string{ + `failed asserting that JSON node "key": has prefix "foo", actual is "value"`, + }, + }, + { + name: "JSON node is string with suffix", + json: `{"key": "value"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithSuffix("lue") + }, + }, + { + name: "JSON node is string with suffix fails", + json: `{"key": "value"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithSuffix("foo") + }, + wantMessages: []string{ + `failed asserting that JSON node "key": has suffix "foo", actual is "value"`, + }, + }, + { + name: "JSON node is string with integer", + json: `{"key": "123"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + }, + { + name: "JSON node is string with integer chain", + json: `{"key": "123"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger().EqualTo(123).GreaterThan(122) + }, + }, + { + name: "JSON node is string with integer zero", + json: `{"key": "0"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + }, + { + name: "JSON node is string with integer negative", + json: `{"key": "-456"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + }, + { + name: "JSON node is string with integer fails", + json: `{"key": "12.3"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + wantMessages: []string{ + `failed asserting that JSON node "key": is string representing integer, actual is "12.3"`, + }, + }, + { + name: "JSON node is string with integer fails non-numeric", + json: `{"key": "abc"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + wantMessages: []string{ + `failed asserting that JSON node "key": is string representing integer, actual is "abc"`, + }, + }, + { + name: "JSON node is string with integer fails empty", + json: `{"key": ""}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithInteger() + }, + wantMessages: []string{ + `failed asserting that JSON node "key": is string representing integer, actual is ""`, + }, + }, + { + name: "JSON node is string with number", + json: `{"key": "123"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber() + }, + }, + { + name: "JSON node is string with number chain", + json: `{"key": "12.5"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber().EqualTo(12.5).GreaterThan(12) + }, + }, + { + name: "JSON node is string with number float", + json: `{"key": "12.5"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber() + }, + }, + { + name: "JSON node is string with number exponent", + json: `{"key": "-1e2"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber() + }, + }, + { + name: "JSON node is string with number fails", + json: `{"key": "abc"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber() + }, + wantMessages: []string{ + `failed asserting that JSON node "key": is string representing number, actual is "abc"`, + }, + }, + { + name: "JSON node is string with number fails empty", + json: `{"key": ""}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsString().WithNumber() + }, + wantMessages: []string{ + `failed asserting that JSON node "key": is string representing number, actual is ""`, + }, + }, { name: "JSON node is string with length", json: `{"key": "value"}`, diff --git a/assertjson/string.go b/assertjson/string.go index 577135d..a5fa9e0 100644 --- a/assertjson/string.go +++ b/assertjson/string.go @@ -238,6 +238,91 @@ func (a *StringAssertion) NotContains(value string, msgAndArgs ...interface{}) * return a } +// WithPrefix asserts that the JSON node has a string value with the given prefix. +func (a *StringAssertion) WithPrefix(prefix string, msgAndArgs ...interface{}) *StringAssertion { + if a == nil { + return nil + } + a.t.Helper() + if !strings.HasPrefix(a.value, prefix) { + a.fail( + fmt.Sprintf(`has prefix "%s", actual is "%s"`, prefix, a.value), + msgAndArgs..., + ) + } + + return a +} + +// WithSuffix asserts that the JSON node has a string value with the given suffix. +func (a *StringAssertion) WithSuffix(suffix string, msgAndArgs ...interface{}) *StringAssertion { + if a == nil { + return nil + } + a.t.Helper() + if !strings.HasSuffix(a.value, suffix) { + a.fail( + fmt.Sprintf(`has suffix "%s", actual is "%s"`, suffix, a.value), + msgAndArgs..., + ) + } + + return a +} + +// WithInteger asserts that the JSON node has a string value that represents a decimal integer. +// It returns IntegerAssertion to execute a chain of assertions for the parsed value. +func (a *StringAssertion) WithInteger(msgAndArgs ...interface{}) *IntegerAssertion { + if a == nil { + return nil + } + a.t.Helper() + parsed, err := strconv.ParseInt(a.value, 10, 64) + if err != nil { + a.fail( + fmt.Sprintf(`is string representing integer, actual is "%s"`, a.value), + msgAndArgs..., + ) + return nil + } + if int64(int(parsed)) != parsed { + a.fail( + fmt.Sprintf(`is string representing integer (overflow), actual is "%s"`, a.value), + msgAndArgs..., + ) + return nil + } + return &IntegerAssertion{ + t: a.t, + message: a.message, + path: a.path, + value: int(parsed), + } +} + +// WithNumber asserts that the JSON node has a string value that represents a number (integer or float). +// It returns NumberAssertion to execute a chain of assertions for the parsed value. +func (a *StringAssertion) WithNumber(msgAndArgs ...interface{}) *NumberAssertion { + if a == nil { + return nil + } + a.t.Helper() + parsed, err := strconv.ParseFloat(a.value, 64) + if err != nil { + a.fail( + fmt.Sprintf(`is string representing number, actual is "%s"`, a.value), + msgAndArgs..., + ) + return nil + } + return &NumberAssertion{ + t: a.t, + message: a.message, + path: a.path, + value: parsed, + } +} + // WithLength asserts that the JSON node has a string value with length equal to the given value. func (a *StringAssertion) WithLength(length int, msgAndArgs ...interface{}) *StringAssertion { if a == nil { From 914edcd2d0e5efc1da1580ab1eb826bb9b2f44df Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 7 Feb 2026 19:40:27 +0300 Subject: [PATCH 7/7] feat: add array of strings assertions to assertjson - Introduced `IsStrings` method to assert that a JSON node is an array of strings. - Added various assertion methods for array length, uniqueness, and content checks. - Updated EXAMPLES.md with new usage examples for array of strings assertions. - Expanded unit tests in assertjson_test.go to cover new assertions and their failure cases. --- EXAMPLES.md | 18 +++ assertjson/assertjson_test.go | 148 +++++++++++++++++ assertjson/strings_array.go | 288 ++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 assertjson/strings_array.go diff --git a/EXAMPLES.md b/EXAMPLES.md index 31c779c..283f58e 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -173,6 +173,24 @@ assertjson.Has(t, data, func(json *assertjson.AssertJSON) { node.IsString().EqualTo("arrayValue") }) + // массив строк (IsStrings) + json.Node("arrayNode").IsStrings() + json.Node("arrayNode").IsStrings().EqualTo("arrayValue") + json.Node("arrayNode").IsStrings().WithUniqueValues() + json.Node("arrayNode").IsStrings().Contains("arrayValue") + json.Node("arrayNode").IsStrings().WithLength(1) + json.Node("arrayNode").IsStrings().WithLengthGreaterThan(0) + json.Node("arrayNode").IsStrings().WithLengthLessThan(2) + json.Node("arrayNode").IsStrings().That(func(values []string) error { + if len(values) == 0 { + return fmt.Errorf("expected non-empty array") + } + return nil + }) + json.Node("arrayNode").IsStrings().Assert(func(tb testing.TB, values []string) { + assert.NotEmpty(tb, values) + }) + json.Node("objectNode").IsObject() json.Node("objectNode").IsObject().WithPropertiesCount(1) json.Node("objectNode").IsObject().WithPropertiesCountGreaterThan(0) diff --git a/assertjson/assertjson_test.go b/assertjson/assertjson_test.go index 9de3b74..5d7ce3b 100644 --- a/assertjson/assertjson_test.go +++ b/assertjson/assertjson_test.go @@ -165,6 +165,28 @@ func TestFileHas(t *testing.T) { node.IsString().EqualTo("arrayValue") }) + // array of strings assertions + json.Node("arrayNode").IsStrings() + json.Node("arrayNode").IsStrings().EqualTo("arrayValue") + json.Node("arrayNode").IsStrings().WithUniqueValues() + json.Node("arrayNode").IsStrings().Contains("arrayValue") + json.Node("arrayNode").IsStrings().WithLength(1) + json.Node("arrayNode").IsStrings().WithLengthGreaterThan(0) + json.Node("arrayNode").IsStrings().WithLengthGreaterThanOrEqual(1) + json.Node("arrayNode").IsStrings().WithLengthLessThan(2) + json.Node("arrayNode").IsStrings().WithLengthLessThanOrEqual(1) + json.Node("arrayNode").IsStrings().That(func(values []string) error { + if len(values) != 1 || values[0] != "arrayValue" { + return fmt.Errorf("expected [arrayValue]") + } + return nil + }) + json.Node("arrayNode").IsStrings().Assert(func(tb testing.TB, values []string) { + tb.Helper() + assert.Equal(tb, []string{"arrayValue"}, values) + }) + assert.Equal(t, 1, json.Node("arrayNode").IsStrings().Length()) + // object assertions json.Node("objectNode").IsObjectWithPropertiesCount(1) json.Node("objectNode").IsObject() @@ -1486,6 +1508,132 @@ func TestHas(t *testing.T) { `failed asserting that JSON node "key" is array`, }, }, + { + name: "JSON node is array of strings", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings() + }, + }, + { + name: "JSON node is array of strings fails on not array", + json: `{"key": "value"}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings() + }, + wantMessages: []string{ + `failed asserting that JSON node "key" is array of strings`, + }, + }, + { + name: "JSON node is array of strings fails on non-string element", + json: `{"key": ["a", 123, "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings() + }, + wantMessages: []string{ + `element at index 1 is not string`, + }, + }, + { + name: "JSON node is array of strings equal to", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().EqualTo("a", "b", "c") + }, + }, + { + name: "JSON node is array of strings equal to fails", + json: `{"key": ["a", "b"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().EqualTo("a", "b", "c") + }, + wantMessages: []string{ + `failed asserting that JSON node "key": equal to`, + }, + }, + { + name: "JSON node is array of strings with unique values", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().WithUniqueValues() + }, + }, + { + name: "JSON node is array of strings with unique values fails", + json: `{"key": ["a", "b", "a"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().WithUniqueValues() + }, + wantMessages: []string{ + `failed asserting that JSON node "key" is array of strings with unique values, duplicated elements`, + }, + }, + { + name: "JSON node is array of strings contains", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().Contains("b") + }, + }, + { + name: "JSON node is array of strings contains fails", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().Contains("x") + }, + wantMessages: []string{ + `array contains "x"`, + }, + }, + { + name: "JSON node is array of strings with length", + json: `{"key": ["a", "b"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().WithLength(2) + }, + }, + { + name: "JSON node is array of strings with length fails", + json: `{"key": ["a", "b"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().WithLength(3) + }, + wantMessages: []string{ + `is array of strings with length is 3, actual is 2`, + }, + }, + { + name: "JSON node is array of strings That", + json: `{"key": ["a"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().That(func(values []string) error { + if len(values) != 1 { + return fmt.Errorf("expected length 1") + } + return nil + }) + }, + }, + { + name: "JSON node is array of strings That fails", + json: `{"key": ["a", "b"]}`, + assert: func(json *assertjson.AssertJSON) { + json.Node("key").IsStrings().That(func(values []string) error { + return fmt.Errorf("custom error") + }) + }, + wantMessages: []string{ + `failed asserting JSON node "key": custom error`, + }, + }, + { + name: "JSON node is array of strings Length", + json: `{"key": ["a", "b", "c"]}`, + assert: func(json *assertjson.AssertJSON) { + assert.Equal(t, 3, json.Node("key").IsStrings().Length()) + }, + }, { name: "JSON node is object", json: `{"key": {"a": 1}}`, diff --git a/assertjson/strings_array.go b/assertjson/strings_array.go new file mode 100644 index 0000000..37728ef --- /dev/null +++ b/assertjson/strings_array.go @@ -0,0 +1,288 @@ +package assertjson + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// IsStrings asserts that the JSON node is an array of strings. +// It returns StringsAssertion to execute a chain of assertions for the node value. +func (node *AssertNode) IsStrings(msgAndArgs ...interface{}) *StringsAssertion { + node.t.Helper() + if node.exists() { + arr, ok := node.value.([]interface{}) + if !ok { + node.fail( + fmt.Sprintf(`failed asserting that JSON node "%s" is array of strings`, node.path.String()), + msgAndArgs..., + ) + return nil + } + values := make([]string, 0, len(arr)) + for i, v := range arr { + s, ok := v.(string) + if !ok { + pathStr := node.path.WithIndex(i).String() + node.fail( + fmt.Sprintf(`failed asserting that JSON node "%s" is array of strings, element at index %d is not string`, pathStr, i), + msgAndArgs..., + ) + return nil + } + values = append(values, s) + } + return &StringsAssertion{ + t: node.t, + message: fmt.Sprintf(`%sfailed asserting that JSON node "%s": `, node.message, node.path.String()), + path: node.path.String(), + value: values, + } + } + + return nil +} + +// StringsAssertion is used to build a chain of assertions for the array of strings node. +type StringsAssertion struct { + t TestingT + message string + path string + value []string +} + +// EqualTo asserts that the array of strings equals the given values (length and elements match). +func (a *StringsAssertion) EqualTo(expected ...string) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if !areStringsEqual(a.value, expected) { + a.fail( + fmt.Sprintf(`equal to [%s], actual is [%s]`, formatStrings(expected), formatStrings(a.value)), + ) + } + + return a +} + +// WithUniqueValues asserts that the array of strings has unique values. +func (a *StringsAssertion) WithUniqueValues(msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + uniques := make(map[string][]int, len(a.value)) + keys := make([]string, 0, len(a.value)) + + for i, s := range a.value { + if _, exist := uniques[s]; !exist { + keys = append(keys, s) + } + uniques[s] = append(uniques[s], i) + } + + duplicates := make([]string, 0) + for _, key := range keys { + if len(uniques[key]) > 1 { + duplicates = append(duplicates, fmt.Sprintf( + "value %s is duplicated at %s", + strconv.Quote(key), + strings.Join(intsToStrings(uniques[key]), ", "), + )) + } + } + + if len(duplicates) > 0 { + a.fail( + fmt.Sprintf( + "failed asserting that JSON node \"%s\" is array of strings with unique values, duplicated elements:\n%s", + a.path, + strings.Join(duplicates, ";\n"), + ), + msgAndArgs..., + ) + } + + return a +} + +// Contains asserts that the array of strings contains the given value. +func (a *StringsAssertion) Contains(value string, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + for _, s := range a.value { + if s == value { + return a + } + } + + a.fail( + fmt.Sprintf(`array contains %s, actual elements are [%s]`, strconv.Quote(value), formatStrings(a.value)), + msgAndArgs..., + ) + + return a +} + +// WithLength asserts that the array of strings has length equal to the given value. +func (a *StringsAssertion) WithLength(expected int, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if len(a.value) != expected { + a.fail( + fmt.Sprintf( + `is array of strings with length is %d, actual is %d`, + expected, + len(a.value), + ), + msgAndArgs..., + ) + } + + return a +} + +// WithLengthGreaterThan asserts that the array of strings has length greater than the value. +func (a *StringsAssertion) WithLengthGreaterThan(expected int, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if len(a.value) <= expected { + a.fail( + fmt.Sprintf( + `is array of strings with length greater than %d, actual is %d`, + expected, + len(a.value), + ), + msgAndArgs..., + ) + } + + return a +} + +// WithLengthGreaterThanOrEqual asserts that the array of strings has length +// greater than or equal to the value. +func (a *StringsAssertion) WithLengthGreaterThanOrEqual(expected int, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if len(a.value) < expected { + a.fail( + fmt.Sprintf( + `is array of strings with length greater than or equal to %d, actual is %d`, + expected, + len(a.value), + ), + msgAndArgs..., + ) + } + + return a +} + +// WithLengthLessThan asserts that the array of strings has length less than the value. +func (a *StringsAssertion) WithLengthLessThan(expected int, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if len(a.value) >= expected { + a.fail( + fmt.Sprintf( + `is array of strings with length less than %d, actual is %d`, + expected, + len(a.value), + ), + msgAndArgs..., + ) + } + + return a +} + +// WithLengthLessThanOrEqual asserts that the array of strings has length +// less than or equal to the value. +func (a *StringsAssertion) WithLengthLessThanOrEqual(expected int, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + if len(a.value) > expected { + a.fail( + fmt.Sprintf( + `is array of strings with length less than or equal to %d, actual is %d`, + expected, + len(a.value), + ), + msgAndArgs..., + ) + } + + return a +} + +// That asserts that the array of strings is satisfied by the callback function. +func (a *StringsAssertion) That(f func(values []string) error, msgAndArgs ...interface{}) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + if err := f(a.value); err != nil { + a.fail( + fmt.Sprintf( + `failed asserting JSON node "%s": %s`, + a.path, + err.Error(), + ), + msgAndArgs..., + ) + } + + return a +} + +// Assert asserts that the array of strings is satisfied by the user function assertFunc. +func (a *StringsAssertion) Assert(assertFunc func(tb testing.TB, values []string)) *StringsAssertion { + if a == nil { + return nil + } + a.t.Helper() + + assertFunc(a.t.(testing.TB), a.value) + + return a +} + +// Length returns the array of strings length. +func (a *StringsAssertion) Length() int { + if a == nil { + return 0 + } + a.t.Helper() + + return len(a.value) +} + +func (a *StringsAssertion) fail(message string, msgAndArgs ...interface{}) { + a.t.Helper() + assert.Fail(a.t, a.message+message, msgAndArgs...) +}