From ca873d087b0e7c6902d62574e5cdb6065e1b3848 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:55:45 +0000 Subject: [PATCH 01/31] chore(deps): update actions/upload-artifact action to v7 --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 89d3442..499e9ee 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -39,7 +39,7 @@ jobs: run: ./gradlew build sonar - name: Upload test report if: failure() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0 with: name: test-report path: build/reports/tests/test From db622900b72d65a9944b7522e7ed13b406aa48fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:45:50 +0000 Subject: [PATCH 02/31] chore(deps): update gradle/actions digest to 342e94e --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74c09fc..e9549c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 with: fetch-depth: 0 - - uses: gradle/actions/wrapper-validation@78f7dee6337f393c0a872d0afbda4c51a20e543d + - uses: gradle/actions/wrapper-validation@342e94ec9aa7226938939f1ad2c761d1245996f5 - name: Set up JDK uses: graalvm/setup-graalvm@9d0e2dff13a8c69a81d72a49c9816836f963fe01 with: From 70d4f829f6838c22a71e1b34fd01db75b91db954 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 02:42:46 +0000 Subject: [PATCH 03/31] chore(deps): update gradle/actions action to v5 --- .github/workflows/Test.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 499e9ee..be6d496 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -28,7 +28,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # 4.4.4 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # 5.0.2 - name: Build and run tests if: ${{ github.event.pull_request.base.ref != 'main' && github.event.action != 'closed' }} run: ./gradlew build @@ -55,4 +55,4 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4 \ No newline at end of file + uses: gradle/actions/dependency-submission@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9549c8..3d6af50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # 4.4.4 + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # 5.0.2 - name: Publish to Maven Central env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} From 51714f88560e4e5ebeb004ee801bda4f055d7e19 Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger <62955101+Jonathan-Zollinger@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:34:40 -0700 Subject: [PATCH 04/31] zollinger/add category support (#77) can listCategories() and createCategories() --- src/main/resources/z4j.yaml | 323 +++++++++++++++++- .../pbu/z4j/client/CategoryClientSpec.groovy | 79 +++++ .../z4j/client/UserSegmentClientSpec.groovy | 5 +- .../z4j/model/CategoriesResponseSpec.groovy | 6 +- 4 files changed, 396 insertions(+), 17 deletions(-) create mode 100644 src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index 9e71288..d7e3760 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -469,6 +469,319 @@ paths: application/json: schema: $ref: '#/components/schemas/TopicsResponse' + /api/v2/help_center/{locale}/categories: + parameters: + - $ref: '#/components/parameters/OptionalLocale' + get: + operationId: ListCategories + tags: + - Category + summary: List Categories by Locale + description: |- +

Allowed for

+ +

The response will list only the categories that the agent, end user, or + anonymous user can view in the help center.

+

Translations are embedded within the category because they're + not shared between resources.

+ parameters: + - name: sort_by + in: query + description: | +

The sort_by parameter can have one of the following values:

+ + + + + + + + + + + + + + + + + + + + + +
valuedescription
positionorder set manually using the Arrange Content page. Default order
created_atorder by creation time
updated_atorder by update time
+ schema: + type: string + enum: + - position + - created_at + - updated_at + - name: sort_order + in: query + description: | + + + + + + + + + + + + + + + + + +
valuedescription
ascascending order
descdescending order
+ schema: + type: string + enum: + - asc + - desc + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CategoriesResponse' + post: + operationId: CreateCategory + tags: + - Category + summary: Create Category by Locale + description: |- +

You must specify a category name and locale. The locale can be omitted if it's specified + in the URL. Optionally, you can specify multiple translations for + the category. The specified locales must be enabled for the current Help Center.

+

Allowed for

+ + requestBody: + content: + application/json: + schema: + type: object + properties: + category: + $ref: '#/components/schemas/Category' + responses: + "201": + description: OK Response + headers: + Location: + description: The URL of the new created category + schema: + type: string + format: url + example: https://{subdomain}.zendesk.com/api/v2/help_center/categories/{category_id}.json + content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + /api/v2/help_center/{locale}/categories/{category_id}: + parameters: + - $ref: '#/components/parameters/OptionalLocale' + - $ref: '#/components/parameters/CategoryId' + get: + operationId: ShowCategory + tags: + - Category + summary: Show Category by Locale + description: |- +

Note: {/locale} is an optional parameter for admins and agents. End users and anonymous users must provide the parameter.

+

Allowed for

+ +

Translations are embedded within the category because they're + not shared between resources.

+ responses: + "200": + description:

description

+ content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + put: + operationId: UpdateCategory + tags: + - Category + summary: Update Category by Locale + description: |- +

These endpoints only update category-level metadata such as the sorting position. + They don't update category translations.

+

Allowed for

+ + responses: + "200": + description: OK Response + content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + delete: + operationId: DeleteCategory + tags: + - Category + summary: Delete Category by Locale + description: |- +

WARNING: Every section and all articles in the category will also be deleted.

+

Allowed for

+ + responses: + "204": + description: No content + /api/v2/help_center/categories/{category_id}/source_locale: + parameters: + - $ref: '#/components/parameters/CategoryId' + put: + operationId: UpdateCategorySourceLocale + tags: + - Category + summary: Update Category Source Locale by Locale + description: |- +

The endpoint updates the category source_locale property

+

Allowed for

+ + responses: + "200": + description:

OK Response

+ content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + /api/v2/help_center/categories: + get: + operationId: ListCategoriesNoLocale + tags: + - Category + summary: List Categories + description: |- +

Allowed for

+ +

The response will list only the categories that the agent can view in the help center.

+ parameters: + - name: sort_by + in: query + description:

Sorts the results by one of the accepted values

+ schema: + type: string + enum: + - position + - created_at + - updated_at + - name: sort_order + in: query + description:

Selects the order of the results.

+ schema: + type: string + enum: + - asc + - desc + responses: + "200": + description:

description

+ content: + application/json: + schema: + $ref: '#/components/schemas/CategoriesResponse' + post: + operationId: CreateCategoryNoLocale + tags: + - Category + summary: Create Category + description: |- +

You must specify a category name and locale. The locale can be omitted if it's specified + in the URL. Optionally, you can specify multiple translations for + the category. The specified locales must be enabled for the current Help Center.

+

Allowed for

+ + responses: + "201": + description: OK Response + headers: + Location: + description: The URL of the new created category + schema: + type: string + format: url + example: https://{subdomain}.zendesk.com/api/v2/help_center/categories/{category_id}.json + content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + /api/v2/help_center/categories/{category_id}: + parameters: + - $ref: '#/components/parameters/CategoryId' + get: + operationId: ShowCategoryNoLocale + tags: + - Category + summary: Show Category + description: |- +

Allowed for

+ + responses: + "200": + description:

description

+ content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + put: + operationId: UpdateCategoryNoLocale + tags: + - Category + summary: Update Category + description: |- +

These endpoints only update category-level metadata such as the sorting position. + They don't update category translations.

+

Allowed for

+ + responses: + "200": + description: OK Response + content: + application/json: + schema: + $ref: '#/components/schemas/CategoryResponse' + delete: + operationId: DeleteCategoryNoLocale + tags: + - Category + summary: Delete Category + description: |- +

WARNING: Every section and all articles in the category will also be deleted.

+

Allowed for

+ + responses: + "204": + description:

No content

components: schemas: Attachment: @@ -914,18 +1227,8 @@ components: type: string description: The API url of this category # readOnly: true - example: - description: "" - html_url: https://company.zendesk.com/hc/en-us/categories/354362577 - id: 1635 - locale: en-us - name: Self Help Articles - source_locale: en-us - url: https://company.zendesk.com/api/v2/help_center/categories/354362577 required: - - id - name - - locale CategoryResponse: type: object properties: diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy new file mode 100644 index 0000000..dc3aef2 --- /dev/null +++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy @@ -0,0 +1,79 @@ +package lol.pbu.z4j.client + +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import lol.pbu.z4j.Z4jSpec +import lol.pbu.z4j.model.* +import spock.lang.Shared + +@MicronautTest +class CategoryClientSpec extends Z4jSpec { + + @Shared + CategoryClient adminCategoryClient, agentCategoryClient, userCategoryClient + + @Shared + List userSegments + + @Shared + List allLocales + + def setupSpec() { + adminCategoryClient = adminCtx.getBean(CategoryClient.class) + agentCategoryClient = agentCtx.getBean(CategoryClient.class) + userCategoryClient = userCtx.getBean(CategoryClient.class) + allLocales = adminCtx.getBean(LocaleClient.class).listLocales().block().locales.collect { it.locale.toLowerCase() } + userSegments = adminCtx.getBean(UserSegmentClient.class).listUserSegments(null).block().getUserSegments() + assert userSegments.size() >= 2 + // built in segments should be at least 2, this is here to just double check this doesn't change + } + + def "can use ListArticles using the '#locale' locale for the #userType user type"(CategoryClient categoryClient, String userType, String locale, ListCategoriesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder) { + when: "query Categories list for the '#locale' locale" + categoryClient.listCategories(locale, sortBy, sortOrder).block() + + then: + noExceptionThrown() + + where: + [[categoryClient, userType], locale, sortBy, sortOrder, startTime, labelNames] << [[[adminCategoryClient, "admin"], [agentCategoryClient, "agent"], [userCategoryClient, "user"]], + allLocales, + [ListCategoriesSortByParameter.values(), null].flatten(), + [ListArticlesSortOrderParameter.values(), null].flatten()].combinations() + } + + def "can use ListCategoriesNoLocale using for the #userType user type"(CategoryClient categoryClient, String userType, ListCategoriesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder) { + when: + categoryClient.listCategoriesNoLocale(sortBy, sortOrder).block() + + then: + noExceptionThrown() + + where: + [[categoryClient, userType], sortBy, sortOrder] << [ + [[adminCategoryClient, "admin"], [agentCategoryClient, "agent"]], + [ListCategoriesSortByParameter.values(), null].flatten(), + [ListArticlesSortOrderParameter.values(), null].flatten() + ].combinations() + } + + def "can use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + + when: "category name to be created is #categoryName" + CategoryResponse response = categoryClient.createCategory(locale, createCategoryRequest).block() + + then: + noExceptionThrown() + + cleanup: "deleting #categoryName from the #locale locale" + categoryClient.deleteCategory(locale, response.getCategory().getId()) + + where: + [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations() + } +} diff --git a/src/test/groovy/lol/pbu/z4j/client/UserSegmentClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/UserSegmentClientSpec.groovy index 99dd1fc..396f88d 100644 --- a/src/test/groovy/lol/pbu/z4j/client/UserSegmentClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/UserSegmentClientSpec.groovy @@ -1,12 +1,9 @@ package lol.pbu.z4j.client -import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus + import io.micronaut.test.extensions.spock.annotation.MicronautTest import lol.pbu.z4j.Z4jSpec import lol.pbu.z4j.model.CreateUserSegmentRequest -import lol.pbu.z4j.model.SectionsResponse -import lol.pbu.z4j.model.TopicsResponse import lol.pbu.z4j.model.UserSegment import lol.pbu.z4j.model.UserSegmentResponse import lol.pbu.z4j.model.UserSegmentsResponse diff --git a/src/test/groovy/lol/pbu/z4j/model/CategoriesResponseSpec.groovy b/src/test/groovy/lol/pbu/z4j/model/CategoriesResponseSpec.groovy index 41465c7..5067e6b 100644 --- a/src/test/groovy/lol/pbu/z4j/model/CategoriesResponseSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/model/CategoriesResponseSpec.groovy @@ -10,7 +10,7 @@ class CategoriesResponseSpec extends Z4jSpec { given: def categoriesResponse = new CategoriesResponse() categoriesResponse.categories == null - def category = new Category(faker.number().randomNumber(), faker.lorem().word(), faker.lorem().sentence()) + def category = new Category(faker.lorem().word()) when: categoriesResponse.addCategoriesItem(category) @@ -23,10 +23,10 @@ class CategoriesResponseSpec extends Z4jSpec { @Unroll def "add categories item to existing list"() { given: - def existingCategory = new Category(faker.number().randomNumber(), faker.lorem().word(), faker.lorem().sentence()) + def existingCategory = new Category(faker.lorem().word()) def categoriesResponse = new CategoriesResponse() categoriesResponse.categories = [existingCategory] - def newCategory = new Category(faker.number().randomNumber(), faker.lorem().word(), faker.lorem().sentence()) + def newCategory = new Category(faker.lorem().word()) when: categoriesResponse.addCategoriesItem(newCategory) From adf74b5e5e6d1e3a39672135b045e1504f977e97 Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger Date: Mon, 2 Mar 2026 21:45:07 -0700 Subject: [PATCH 05/31] ci: run only on ubuntu --- .github/workflows/Test.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index be6d496..45029cd 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -6,10 +6,7 @@ on: - "[0-9]+.[0-9]+.[0-9]+" # release branches jobs: build-and-analyze: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + runs-on: ubuntu-latest env: Z4J_URL: ${{ secrets.ZENDESK_URL }} Z4J_TOKEN: ${{ secrets.ZENDESK_TOKEN }} From 2596765e7fc229619ea34b26edbf83aba3fffacd Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger Date: Mon, 2 Mar 2026 23:06:25 -0700 Subject: [PATCH 06/31] fix: correct infrequent 409 errors --- build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5f3a05d..3317200 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation("io.micronaut.reactor:micronaut-reactor-http-client") implementation("io.micronaut.serde:micronaut-serde-jackson") implementation("io.micronaut.validation:micronaut-validation") + implementation("io.micronaut:micronaut-retry") "lombok"("org.projectlombok:lombok:${lombokVersion}") runtimeOnly("org.yaml:snakeyaml") testImplementation("net.datafaker:datafaker:$dataFakerVersion") @@ -60,7 +61,7 @@ micronaut { annotations("lol.pbu.*") } openapi { - version = "6.19.3" + version = "6.20.0" client(file("src/main/resources/z4j.yaml")) { apiPackageName.set("lol.pbu.z4j.client") modelPackageName.set("lol.pbu.z4j.model") @@ -71,6 +72,7 @@ micronaut { apiNameSuffix.set("Client") alwaysUseGenerateHttpResponse.set(false) generateHttpResponseWhereRequired.set(false) + additionalProperties.put("retryable", "true") } } } From 930d82fc111a443c2022cae8f6c68c136af89030 Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger Date: Mon, 2 Mar 2026 23:08:58 -0700 Subject: [PATCH 07/31] test: check create, delete category --- .../pbu/z4j/client/CategoryClientSpec.groovy | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy index dc3aef2..3a4d33c 100644 --- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy @@ -1,10 +1,14 @@ package lol.pbu.z4j.client + +import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.extensions.spock.annotation.MicronautTest import lol.pbu.z4j.Z4jSpec import lol.pbu.z4j.model.* import spock.lang.Shared +import static io.micronaut.http.HttpStatus.FORBIDDEN + @MicronautTest class CategoryClientSpec extends Z4jSpec { @@ -76,4 +80,75 @@ class CategoryClientSpec extends Z4jSpec { where: [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations() } + def "cannot use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + + when: "category name to be created is #categoryName" + CategoryResponse response = categoryClient.createCategory(locale, createCategoryRequest).block() + + then: + HttpClientResponseException error = thrown(HttpClientResponseException) + + and: + error.getStatus() == FORBIDDEN + + cleanup: "deleting #categoryName from the #locale locale" + try { + adminCategoryClient.deleteCategory(locale, response.getCategory().getId()) + } catch (NullPointerException ignored) {} + + where: + [[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations() + } + + def "can use DeleteCategory as an #userType for the '#locale"(CategoryClient categoryClient, String userType, String locale) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.bluey().quote() + Category category = new Category(categoryName) + category.setDescription(faker.lordOfTheRings().location()) + createCategoryRequest.setCategory(category) + CategoryResponse response = categoryClient.createCategory(locale, createCategoryRequest).block() + + when: + categoryClient.deleteCategory(locale, response.getCategory().getId()) + + then: + noExceptionThrown() + + where: + [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations() + } + + def "cannot use DeleteCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.bluey().quote() + " " + UUID.randomUUID().toString() + Category category = new Category(categoryName) + category.setDescription(faker.lordOfTheRings().location()) + createCategoryRequest.setCategory(category) + CategoryResponse response = adminCategoryClient.createCategory(locale, createCategoryRequest).block() + + when: + categoryClient.deleteCategory(locale, response.getCategory().getId()) + + then: + noExceptionThrown() // this shouldn't be allowed! +// HttpClientResponseException error = thrown(HttpClientResponseException) +// and: +// error.getStatus() == FORBIDDEN + + cleanup: + try { + adminCategoryClient.deleteCategory(locale, response.getCategory().getId()) + }catch (NullPointerException ignored){} + + where: + [[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations() + } } From 82bff97d489346c43486108d0ebfdf2d7f3e29b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:53:01 +0000 Subject: [PATCH 08/31] chore(deps): update gradle/actions digest to c2457a7 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d6af50..f1e93ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 with: fetch-depth: 0 - - uses: gradle/actions/wrapper-validation@342e94ec9aa7226938939f1ad2c761d1245996f5 + - uses: gradle/actions/wrapper-validation@c2457a7fb283d2bfe1c94ac0c360c27a79bcffd4 - name: Set up JDK uses: graalvm/setup-graalvm@9d0e2dff13a8c69a81d72a49c9816836f963fe01 with: From 3fd86dd77c0176c5f309f8cedb00f4bd84243f37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:50:09 +0000 Subject: [PATCH 09/31] chore(deps): update gradle/actions action to v6 --- .github/workflows/Test.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 45029cd..285053e 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,7 +25,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # 5.0.2 + uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # 6.0.0 - name: Build and run tests if: ${{ github.event.pull_request.base.ref != 'main' && github.event.action != 'closed' }} run: ./gradlew build @@ -52,4 +52,4 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 \ No newline at end of file + uses: gradle/actions/dependency-submission@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1e93ec..33544cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # 5.0.2 + uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # 6.0.0 - name: Publish to Maven Central env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} From 8925373435f4c9f0a0f9921f26adca70b19ac500 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:56:09 +0000 Subject: [PATCH 10/31] chore(deps): update graalvm/setup-graalvm digest to 03e8abf --- .github/workflows/Test.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 285053e..cf9a901 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 # SonarCloud needs a full history to assign issues correctly - name: Set up JDK - uses: graalvm/setup-graalvm@9d0e2dff13a8c69a81d72a49c9816836f963fe01 + uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 with: java-version: '21' distribution: 'graalvm-community' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33544cc..4961a11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: gradle/actions/wrapper-validation@c2457a7fb283d2bfe1c94ac0c360c27a79bcffd4 - name: Set up JDK - uses: graalvm/setup-graalvm@9d0e2dff13a8c69a81d72a49c9816836f963fe01 + uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 with: java-version: '21' distribution: 'graalvm-community' From 97701eb4449e2c89ea1b0aa235b66f1b49b4a073 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 01:34:09 +0000 Subject: [PATCH 11/31] fix(deps): update dependency org.projectlombok:lombok to v1.18.44 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5beb955..5251aed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ micronautVersion=4.8.3 z4jVersion=0.1.0 nettyVersion=4.1.124.Final -lombokVersion=1.18.42 +lombokVersion=1.18.44 dataFakerVersion=2.5.4 org.gradle.jvmargs=-Xmx4096M # SonarQube Project Configuration From 9c9b68a550ea50233bddd814bd2c18316680f5f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:11:03 +0000 Subject: [PATCH 12/31] chore(deps): update graalvm/setup-graalvm action to v1.5.0 --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index cf9a901..0d61e83 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - name: Set up JDK 21 - uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # 1.4.5 + uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 # 1.5.0 with: java-version: '21' distribution: 'graalvm-community' From 26bdcae6bffa565f900c2561caa0f5795c6fb74d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:01:33 +0000 Subject: [PATCH 13/31] chore(deps): update graalvm/setup-graalvm action to v1.5.1 --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 0d61e83..3a2de4f 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - name: Set up JDK 21 - uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 # 1.5.0 + uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 # 1.5.1 with: java-version: '21' distribution: 'graalvm-community' From 63726054034cbd86c62aef42f29caad4a0128b0f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:42:10 +0000 Subject: [PATCH 14/31] chore(deps): update gradle/actions action to v6.0.1 --- .github/workflows/Test.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 3a2de4f..bad0c27 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,7 +25,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # 6.0.0 + uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # 6.0.1 - name: Build and run tests if: ${{ github.event.pull_request.base.ref != 'main' && github.event.action != 'closed' }} run: ./gradlew build @@ -52,4 +52,4 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # v6.0.0 \ No newline at end of file + uses: gradle/actions/dependency-submission@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4961a11..e29497c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@0f4528296b4bc09e8ae0fc7be30185a4ab435545 # 6.0.0 + uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # 6.0.1 - name: Publish to Maven Central env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} From 1c54cd411034444658a53f0e744b675769155d05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:51:36 +0000 Subject: [PATCH 15/31] chore(deps): update gradle/actions digest to 263d8fe --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e29497c..b169f02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 with: fetch-depth: 0 - - uses: gradle/actions/wrapper-validation@c2457a7fb283d2bfe1c94ac0c360c27a79bcffd4 + - uses: gradle/actions/wrapper-validation@263d8fe18eb54ae581bcdc2e263c5a49173958a3 - name: Set up JDK uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 with: From f62ae39811862937b2c10f6da74a974bc3fb4a9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:01:29 +0000 Subject: [PATCH 16/31] chore(deps): update graalvm/setup-graalvm digest to 2149f39 --- .github/workflows/Test.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index bad0c27..383373e 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 # SonarCloud needs a full history to assign issues correctly - name: Set up JDK - uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 + uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 with: java-version: '21' distribution: 'graalvm-community' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b169f02..4eed6dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: gradle/actions/wrapper-validation@263d8fe18eb54ae581bcdc2e263c5a49173958a3 - name: Set up JDK - uses: graalvm/setup-graalvm@03e8abf916fd0e281b2efe7b2da3378bb0a1d085 + uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 with: java-version: '21' distribution: 'graalvm-community' From 768c37ccc3c09d6d914901efc9881ff0b56783f2 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 31 Mar 2026 10:31:57 -0600 Subject: [PATCH 17/31] chore: bump micronaut to 4.10.10 --- build.gradle.kts | 1 - gradle.properties | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3317200..0e1dab2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,6 @@ version = project.properties["z4jVersion"]!! val dataFakerVersion = project.properties["dataFakerVersion"]!! val lombokVersion = project.properties["lombokVersion"]!! -extra["netty.version"] = project.properties["nettyVersion"]!! configurations.create("lombok") diff --git a/gradle.properties b/gradle.properties index 5251aed..d041e86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,5 @@ -micronautVersion=4.8.3 +micronautVersion=4.10.10 z4jVersion=0.1.0 -nettyVersion=4.1.124.Final lombokVersion=1.18.44 dataFakerVersion=2.5.4 org.gradle.jvmargs=-Xmx4096M From 1bce330b813dd58d5c9877473b2862e69afa510b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:24:26 +0000 Subject: [PATCH 18/31] chore(deps): update gradle/actions action to v6.1.0 --- .github/workflows/Test.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 383373e..30885a7 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,7 +25,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # 6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # 6.1.0 - name: Build and run tests if: ${{ github.event.pull_request.base.ref != 'main' && github.event.action != 'closed' }} run: ./gradlew build @@ -52,4 +52,4 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 \ No newline at end of file + uses: gradle/actions/dependency-submission@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4eed6dd..85d8aed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: java-version: '21' distribution: 'graalvm-community' - name: Setup Gradle - uses: gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # 6.0.1 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # 6.1.0 - name: Publish to Maven Central env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} From abfbd25e6087d61b3f94c24606a1fce5149b5fa3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:10:50 +0000 Subject: [PATCH 19/31] chore(deps): update graalvm/setup-graalvm action to v1.5.2 --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 30885a7..52fd266 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -47,7 +47,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 - name: Set up JDK 21 - uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 # 1.5.1 + uses: graalvm/setup-graalvm@60c26726de13f8b90771df4bc1641a52a3159994 # 1.5.2 with: java-version: '21' distribution: 'graalvm-community' From b1db1f38ef1e43f760c12477afea147267af6f8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:10:45 +0000 Subject: [PATCH 20/31] chore(deps): update graalvm/setup-graalvm digest to 60c2672 --- .github/workflows/Test.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 52fd266..b489e3f 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -20,7 +20,7 @@ jobs: with: fetch-depth: 0 # SonarCloud needs a full history to assign issues correctly - name: Set up JDK - uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 + uses: graalvm/setup-graalvm@60c26726de13f8b90771df4bc1641a52a3159994 with: java-version: '21' distribution: 'graalvm-community' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85d8aed..b084397 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: gradle/actions/wrapper-validation@263d8fe18eb54ae581bcdc2e263c5a49173958a3 - name: Set up JDK - uses: graalvm/setup-graalvm@2149f395d36ce12ad4ee5d7f334b26bf081fa555 + uses: graalvm/setup-graalvm@60c26726de13f8b90771df4bc1641a52a3159994 with: java-version: '21' distribution: 'graalvm-community' From 65aff240a51dbfa25460845c31aa32c0cab2b9cd Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 31 Mar 2026 11:16:09 -0600 Subject: [PATCH 21/31] refactor: cleanup whitespace --- gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d041e86..11d0493 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,5 +13,4 @@ systemProp.sonar.tests=src/test/groovy systemProp.sonar.junit.reportPaths=build/test-results/test systemProp.sonar.coverage.jacoco.xmlReportPath=build/reports/jacoco/test/jacocoTestReport.xml systemProp.sonar.java.binaries=build/classes/java/main -systemProp.sonar.java.test.binaries=build/classes/groovy/test - +systemProp.sonar.java.test.binaries=build/classes/groovy/test \ No newline at end of file From cb1d6bc4a1355bf4e1d9b96e2f8e68ce1d3b6075 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 31 Mar 2026 22:38:53 -0600 Subject: [PATCH 22/31] feat: introduce searchClient#list() --- src/main/resources/z4j.yaml | 154 +++++++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 49 ++++++ 2 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index d7e3760..42715b5 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -575,7 +575,7 @@ paths: $ref: '#/components/schemas/Category' responses: "201": - description: OK Response + description: Created response headers: Location: description: The URL of the new created category @@ -782,12 +782,83 @@ paths: responses: "204": description:

No content

+ /api/v2/search: + get: + operationId: List + tags: + - Search + summary: List Search Results + description: |- +

Returns the search results. See Query syntax for details on the {@code query} parameter.

+

Use the ampersand character (&) to append the {@code sort_by} or {@code sort_order} parameters to the URL.

+

For examples, see Searching with Zendesk API.

+

This endpoint has its own rate limit. The rate limit counts towards the global API rate limit. See Limits.

+

Allowed For

+
    +
  • Agents
  • +
+ + parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + - name: sort_by + in: query + description: One of {@code updated_at}, {@code created_at}, {@code priority}, {@code status}, or {@code ticket_type}. Defaults to sorting by relevance + schema: + $ref: '#/components/schemas/SearchSortBy' + - name: sort_order + in: query + description: Defaults to descending + schema: + $ref: '#/components/schemas/SearchSortOrder' + - $ref: '#/components/parameters/SearchInclude' + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: + SearchSortBy: + type: string + enum: + - updated_at + - created_at + - priority + - status + - ticket_type + x-enum-name: SearchSortBy + x-enum-varnames: + - UPDATED_AT + - CREATED_AT + - PRIORITY + - STATUS + - TICKET_TYPE + SearchSortOrder: + type: string + enum: + - asc + - desc + x-enum-name: SearchSortOrder + x-enum-varnames: + - ASCENDING + - DESCENDING Attachment: type: object description: | -

A file represented as an Attachment object

+ A file represented as an Attachment object allOf: - $ref: '#/components/schemas/AttachmentBase' - $ref: '#/components/schemas/AttachmentThumbnails' @@ -801,7 +872,7 @@ components: content_url: type: string description: | -

A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties

+ A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties readOnly: true deleted: type: boolean @@ -832,6 +903,17 @@ components: malware_scan_result: type: string description: 'The result of the malware scan. There is a delay between the time the attachment is uploaded and when the malware scan is completed. Usually the scan is done within a few seconds, but high load conditions can delay the scan results. Possible values: "malware_found", "malware_not_found", "failed_to_scan", "not_scanned"' + enum: + - malware_found + - malware_not_found + - failed_to_scan + - not_scanned + x-enum-name: MalwareScanResult + x-enum-varnames: + - MALWARE_FOUND + - MALWARE_NOT_FOUND + - FAILED_TO_SCAN + - NOT_SCANNED readOnly: true mapped_content_url: type: string @@ -3259,6 +3341,64 @@ components: type: array items: $ref: '#/components/schemas/Vote' + SearchResult: + type: object + properties: + created_at: + type: string + description: When the resource was created + default: + type: boolean + description: Flag to indicate whether this is the default resource + deleted: + type: boolean + description: Flag to indicate whether or not resource has been deleted + description: + type: string + description: The description of the resource + id: + type: integer + description: The ID of the resource + name: + type: string + description: The name of the resource + result_type: + type: string + description: The type of the resource + updated_at: + type: string + description: When the resource was last updated + url: + type: string + description: The url of the resource + SearchResponse: + type: object + properties: + count: + type: integer + description: The number of resources returned by the query corresponding to this page of results in the paginated response + readOnly: true + facets: + type: string + description: The facets corresponding to the search query + nullable: true + readOnly: true + next_page: + type: string + description: URL to the next page of results + nullable: true + readOnly: true + previous_page: + type: string + description: URL to the previous page of results + nullable: true + readOnly: true + results: + type: array + description: May consist of tickets, users, groups, or organizations, as specified by the `result_type` property in each result object + items: + $ref: '#/components/schemas/SearchResult' + readOnly: true parameters: ArticleAttachmentId: name: article_attachment_id @@ -3352,6 +3492,14 @@ components: type: string example: en-us example: en-us + SearchInclude: + name: include + in: query + description: | + Sideloads to include in the response. Accepts a comma-separated list of values. + The available sideloads depend on the search result types. + schema: + type: string SectionId: name: section_id in: path diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy new file mode 100644 index 0000000..356c81d --- /dev/null +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -0,0 +1,49 @@ +package lol.pbu.z4j.client + +import io.micronaut.http.client.exceptions.HttpClientResponseException +import lol.pbu.z4j.Z4jSpec +import lol.pbu.z4j.model.SearchSortBy +import lol.pbu.z4j.model.SearchSortOrder +import spock.lang.Shared +import spock.lang.Unroll + +class SearchClientSpec extends Z4jSpec { + @Shared + SearchClient adminSearchClient, agentSearchClient, userSearchClient + + def setupSpec() { + adminSearchClient = adminCtx.getBean(SearchClient.class) + agentSearchClient = agentCtx.getBean(SearchClient.class) + userSearchClient = userCtx.getBean(SearchClient.class) + } + + @Unroll("an #clientName user can run the list method with sortby: #sortBy, sortOrder: #sortOrder and include: #include") + void "can run the list method"(String clientName,SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + when: + client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() + + then: + noExceptionThrown() + + where: + [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include") + void "cannot run searchClient.list()"(SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + when: + client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() + + then: + thrown(HttpClientResponseException) + + where: + [client, sortBy, sortOrder, include] << [[userSearchClient], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } +} From c0f6a26c029f413865576f0707a5561fc3ce82a2 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Wed, 1 Apr 2026 23:09:57 -0600 Subject: [PATCH 23/31] feat: introduce Search#Count method --- src/main/resources/z4j.yaml | 34 +++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 46 +++++++++++++++---- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index 42715b5..c6f5288 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -797,7 +797,6 @@ paths:
  • Agents
- parameters: - name: query in: query @@ -829,6 +828,39 @@ paths: application/json: schema: $ref: '#/components/schemas/BadRequestErrorResponse' + /api/v2/search/count: + get: + operationId: Count + tags: + - Search + summary: Show Search Results Count + description: | + Returns the number of items matching the query rather than returning the items. The search string works the same as a regular search. + +

Allowed For

+
    +
  • Agents
  • +
+ parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: SearchSortBy: diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index 356c81d..09e03d7 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -18,18 +18,35 @@ class SearchClientSpec extends Z4jSpec { } @Unroll("an #clientName user can run the list method with sortby: #sortBy, sortOrder: #sortOrder and include: #include") - void "can run the list method"(String clientName,SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + void "can run the list method"(String clientName, SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { when: client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() then: noExceptionThrown() + when: + client.count(faker.bluey().quote()).block() + + then: + noExceptionThrown() + where: [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], - [SearchSortBy.values(), null].flatten(), - [SearchSortOrder.values(), null].flatten(), - [null, faker.cat().name()]].combinations() + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + void "an #clientName user can run the count method"(String clientName, SearchClient client) { + when: + client.count(faker.bluey().quote()).block() + + then: + noExceptionThrown() + + where: + [[client, clientName]] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] } @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include") @@ -41,9 +58,22 @@ class SearchClientSpec extends Z4jSpec { thrown(HttpClientResponseException) where: - [client, sortBy, sortOrder, include] << [[userSearchClient], - [SearchSortBy.values(), null].flatten(), - [SearchSortOrder.values(), null].flatten(), - [null, faker.cat().name()]].combinations() + [client, sortBy, sortOrder, include] << [[userSearchClient], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + void "a normal user cannot run the count method"(SearchClient client) { + when: + client.count(faker.bluey().quote()).block() + + then: + thrown(HttpClientResponseException) + + where: + client | _ + userSearchClient | _ + } } From b17ec6d5cc65f4517bd996b4522de808b4802319 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 4 Apr 2026 19:58:45 -0600 Subject: [PATCH 24/31] feat: add SearchClient#export() --- src/main/resources/z4j.yaml | 92 ++++++++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 21 +++-- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index c6f5288..ec029e8 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -797,10 +797,18 @@ paths:
  • Agents
+

Pagination

+
    +
  • Offset pagination only
  • +
+

Offset pagination may result in duplicate results when paging. You can also use the + Export Search Results endpoint, which + uses cursor-based pagination and doesn't return duplicate results. See + Using cursor pagination for more information.

parameters: - name: query in: query - description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. required: true schema: type: string @@ -861,6 +869,75 @@ paths: application/json: schema: $ref: '#/components/schemas/BadRequestErrorResponse' + /api/v2/search/export: + get: + operationId: Export + tags: + - Search + summary: Export Search Results + description: |- +

Exports a set of results. See Query syntax for the syntax of the {@code query} parameter.

+

Use this endpoint for search queries that will return more than 1000 results. The result set is ordered only by the {@code created_at} attribute.

+

The search only returns results of a single object type. The following object types are supported: ticket, organization, user, or group.

+

You must specify the type in the {@code filter[type]} parameter. Searches with type in the query string will result in an error.

+

Allowed For

+
    +
  • Agents
  • +
+

Pagination

+
    +
  • Cursor pagination
  • +
+

See Pagination.

+

Returns a maximum of 1000 records per page. The number of results shown in a page is determined by the {@code page[size]} parameter.

+

Note: You may experience a speed reduction or a timeout if you request 1000 results per page and you have many archived tickets in the results. Try reducing the number of results per page. We recommend 100 results per page.

+

The cursor specified by the {@code after_cursor} property in a response expires after one hour.

+

For more information on cursor-based pagination, see the following articles:

+ +

Export Search Results Limits

+

This API endpoint is rate-limited to 100 requests per minute per account. The limit also counts towards the global API rate limit.

+ parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + - name: page[size] + in: query + description: The number of results shown in a page. + required: true + schema: + type: integer + - name: page[after] + in: query + description: The cursor token for fetching the next page of results. + required: true + schema: + type: string + - name: filter[type] + in: query + description: The object type returned by the export query. Can be `ticket`, `organization`, `user`, or `group`. + required: true + schema: + $ref: '#/components/schemas/SearchExportType' + - $ref: '#/components/parameters/SearchInclude' + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: SearchSortBy: @@ -887,6 +964,19 @@ components: x-enum-varnames: - ASCENDING - DESCENDING + SearchExportType: + type: string + enum: + - ticket + - organization + - user + - group + x-enum-name: SearchExportType + x-enum-varnames: + - TICKET + - ORGANIZATION + - USER + - GROUP Attachment: type: object description: | diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index 09e03d7..fc42953 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -2,6 +2,7 @@ package lol.pbu.z4j.client import io.micronaut.http.client.exceptions.HttpClientResponseException import lol.pbu.z4j.Z4jSpec +import lol.pbu.z4j.model.SearchExportType import lol.pbu.z4j.model.SearchSortBy import lol.pbu.z4j.model.SearchSortOrder import spock.lang.Shared @@ -25,12 +26,6 @@ class SearchClientSpec extends Z4jSpec { then: noExceptionThrown() - when: - client.count(faker.bluey().quote()).block() - - then: - noExceptionThrown() - where: [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], [SearchSortBy.values(), null].flatten(), @@ -74,6 +69,20 @@ class SearchClientSpec extends Z4jSpec { where: client | _ userSearchClient | _ + } + void "an #clientName can call export method with pageSize: #pageSize, pageAfter: #pageAfter, filterType: #filterType and include: #include"(String clientName, SearchClient client, int pageSize, String pageAfter, SearchExportType filterType, String include) { + when: + client.export(faker.bluey().quote(), pageSize, pageAfter, filterType, include).block() + + then: + noExceptionThrown() + + where: + [[client, clientName], pageSize, pageAfter, filterType, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], + [100], + [faker.internet().uuid()], + SearchExportType.values(), + ["organizations"]].combinations() } } From 5f8c00f0c2388fd95478b77899f2a66c768be42f Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 4 Apr 2026 23:13:34 -0600 Subject: [PATCH 25/31] fix(test): correct where block syntax --- src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index fc42953..b0f12e7 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -41,7 +41,7 @@ class SearchClientSpec extends Z4jSpec { noExceptionThrown() where: - [[client, clientName]] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] + [client, clientName] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] } @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include") From 955f489689fcfa62e2844c81252edfbedcc22619 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:58:10 +0000 Subject: [PATCH 26/31] chore(deps): update gradle/actions digest to 11d4d83 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b084397..ab0c18d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 with: fetch-depth: 0 - - uses: gradle/actions/wrapper-validation@263d8fe18eb54ae581bcdc2e263c5a49173958a3 + - uses: gradle/actions/wrapper-validation@11d4d83c63a6ce61b32d8a9c4faddbdb04fe9917 - name: Set up JDK uses: graalvm/setup-graalvm@60c26726de13f8b90771df4bc1641a52a3159994 with: From 145eb00212e343daf1f1049f85c95a5be5ca4254 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:27:34 +0000 Subject: [PATCH 27/31] chore(config): migrate config renovate.json --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 6f2f5ef..c811878 100644 --- a/renovate.json +++ b/renovate.json @@ -3,7 +3,7 @@ "extends": [ "config:recommended" ], - "baseBranches": [ + "baseBranchPatterns": [ "0.1.1" ] } From 987731b91d7619730193b490495725dac8ca91c6 Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger <62955101+Jonathan-Zollinger@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:40:24 -0600 Subject: [PATCH 28/31] Create LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From fc2603680f05a8e6d5cc59a75629c367bb5b503a Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger Date: Sat, 11 Apr 2026 14:53:58 -0600 Subject: [PATCH 29/31] chore: bump project version to 0.1.1-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 11d0493..1c4bd25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ micronautVersion=4.10.10 -z4jVersion=0.1.0 +z4jVersion=0.1.1-SNAPSHOT lombokVersion=1.18.44 dataFakerVersion=2.5.4 org.gradle.jvmargs=-Xmx4096M From 6a2ccd868c72529b007a6f48126638cb2f259308 Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger <62955101+Jonathan-Zollinger@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:17:20 -0600 Subject: [PATCH 30/31] Add support for Zendesk Categories No support for the update category source locale endpoint. No support for creating a category and translations at the same time (defect with zendesk api) --- CONTRIBUTING.md | 31 +- build.gradle.kts | 6 +- cleanup_categories.ps1 | 77 +++++ cleanup_user_segments.ps1 | 18 +- src/main/resources/z4j.yaml | 300 ++++++++++++------ src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy | 1 + .../pbu/z4j/client/ArticleClientSpec.groovy | 7 +- .../pbu/z4j/client/CategoryClientSpec.groovy | 150 ++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 1 + .../pbu/z4j/client/TicketClientSpec.groovy | 5 - .../z4j/model/TranslationsResponseSpec.groovy | 22 +- src/test/resources/Application-test.yaml | 8 + src/test/resources/logback.xml | 13 + 13 files changed, 488 insertions(+), 151 deletions(-) create mode 100644 cleanup_categories.ps1 create mode 100644 src/test/resources/Application-test.yaml create mode 100644 src/test/resources/logback.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1b7396..8f40fd4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,22 +4,23 @@ By participating in this project, you agree to abide our [code of conduct] -**✨ Thank you for contributing to z4j! ✨** +Thank you for contributing to z4j -PBU projects are open to contributions! Below are some instructions on best practices and standards used when contributing to this project! +Below are some instructions on best practices and standards used when contributing to this project! ## Style Guide - This project uses [google's java style guide]. - We follow (and enforce) [conventional commits] in this repo. ## Set up your machine -`z4j` is written in java 21, runs on [graal community distro], and uses [gradle] as its build tool. +`z4j` is written in java 21, compiled with the [graalvm], and uses [gradle] as its build tool. -### Prerequisites: +### What you need locally: - Gradle doesn't need to be installed locally, a [gradle wrapper] is provided with this repo. - Docker or Podman installed and running at compile time - [Graal-CE 21] - [Git] +- a decent IDE like IntelliJ #### Getting Started Create your own fork of `z4j`, clone your fork and call the gradle wrapper to build the project @@ -30,7 +31,7 @@ cd z4j ./gradlew build ``` ## IDE -Any IDE specific documentation will reference IntelliJ configured to [delegate build and run actions to gradle]. +Any IDE specific documentation in this repo will reference IntelliJ, specifically one configured to [delegate build and run actions to gradle]. # Testing @@ -45,6 +46,8 @@ To run the tests, you will need: 1. A Zendesk account with the Help Center activated, along with an API token for access. 2. Users with [different roles] created in your Zendesk instance. 3. [Environment variables] configured in your test environment +4. [Custom ticket fields] configured in your sandbox +5. [Expected locales] enabled in your help center. ### Required Roles for Testing @@ -58,6 +61,8 @@ You'll need to set up the following users in your Zendesk account:
User Configuration View a user's configured role by navigating to {domain}.zendesk.com/admin/people/team/members, then selecting a user. + + @@ -127,6 +132,7 @@ A dropdown menu to categorize the ticket's subject. * **Field ID**: `40971535122835` **Field values:** + | Field option title | Tag | | :----------------- | :---- | | Delivery | `delivery` | @@ -142,6 +148,7 @@ A dropdown menu to specify the type of refund being processed. * **Field ID**: `45241019372563` **Field values:** + | Field option title | Tag | Default | | :----------------- | :---- |:---:| | Full Refund | `full_refund` | | @@ -217,6 +224,16 @@ A multi-select dropdown for reporting one or more errors. --- +## Expected Locales +A few locales are to be enabled. See Zendesk's current docs on how to do that. + +The following languages are required for testing: +- Spanish (`es`) +- French (`fr`) +- English (`en`) + +--- + ## Testing Strategy One of the most important parts of contributing to z4j is getting tests right. A test is written adequately if: @@ -292,6 +309,8 @@ This method is a little tricky to test for negative tests because the only way t [branches of code]:https://medium.com/@zubairkhansh/branch-testing-and-branch-coverage-3fb4bbd9f949 [code of conduct]:CODE_OF_CONDUCT.md [conventional commits]:https://www.conventionalcommits.org/en/v1.0.0/ +[Custom ticket fields]:#custom-ticket-fields-setup +[Expected locales]:#Expected-Locales [delegate build and run actions to gradle]:https://www.jetbrains.com/help/idea/work-with-gradle-projects.html#delegate_build_gradle [different roles]:#Required-Roles-for-Testing [google's java style guide]:https://google.github.io/styleguide/javaguide.html @@ -299,7 +318,7 @@ This method is a little tricky to test for negative tests because the only way t [gradle]:https://gradle.org/maven-and-gradle/ [gradle wrapper]:https://docs.gradle.org/current/userguide/gradle_wrapper_basics.html [Git]:https://gist.github.com/Jonathan-Zollinger/8d9a231a57f3d33ff813989c34df00e0 -[graal community distro]:https://www.graalvm.org/release-notes/JDK_21/ +[graalvm]:https://www.graalvm.org/release-notes/JDK_21/ [Graal-CE 21]:https://www.graalvm.org/jdk21/docs/ [Environment Variables]:#Required-Environment-Variables [source function]:https://gist.github.com/Jonathan-Zollinger/96160f971741f5f3a8749d10127e7764 diff --git a/build.gradle.kts b/build.gradle.kts index 0e1dab2..51ef51a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { "lombok"("org.projectlombok:lombok:${lombokVersion}") runtimeOnly("org.yaml:snakeyaml") testImplementation("net.datafaker:datafaker:$dataFakerVersion") + testImplementation("ch.qos.logback:logback-classic") } java { @@ -87,11 +88,6 @@ tasks.jacocoTestReport { xml.required.set(true) html.required.set(true) } - classDirectories.setFrom(files(classDirectories.files.map { - fileTree(it) { - exclude("lol/pbu/Application.class") - } - })) } tasks.test { diff --git a/cleanup_categories.ps1 b/cleanup_categories.ps1 new file mode 100644 index 0000000..9cab475 --- /dev/null +++ b/cleanup_categories.ps1 @@ -0,0 +1,77 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + This script paginates through all pages of the GET /api/v2/help_center/categories endpoint and deletes all categories. +.DESCRIPTION + This script will first check for the presence of the following environment variables: + - Z4J_URL: The URL of your Zendesk instance (e.g., https://your-subdomain.zendesk.com) + - Z4J_TOKEN: Your Zendesk API token. + - Z4J_ADMIN_EMAIL: The email address of a Zendesk admin. + + If all environment variables are present, it will then: + 1. Paginate through all categories. + 2. Delete each of those categories. +.NOTES + Author: Jonathan + Date: $(Get-Date -Format "yyyy-MM-dd") +#> + +param() + +#region Environment Variable Validation +$requiredEnvVars = @("Z4J_URL", "Z4J_TOKEN", "Z4J_ADMIN_EMAIL") +$missingEnvVars = @() + +foreach ($var in $requiredEnvVars) { + if (-not (Test-Path "env:$var")) { + $missingEnvVars += $var + } +} + +if ($missingEnvVars.Count -gt 0) { + Write-Error "The following environment variables are not set: $($missingEnvVars -join ', ')" + exit 1 +} +#endregion + +$Z4JUrl = $env:Z4J_URL +$apiToken = $env:Z4J_TOKEN +$adminEmail = $env:Z4J_ADMIN_EMAIL + +$credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${adminEmail}/token:${apiToken}")) +$headers = @{ + "Authorization" = "Basic $credentials" + "Content-Type" = "application/json" +} + +$nextPage = "$Z4JUrl/api/v2/help_center/categories" + +do { + try { + Write-Host "Fetching categories from $nextPage" + $response = Invoke-RestMethod -Uri $nextPage -Method Get -Headers $headers + + if ($null -ne $response.categories) { + foreach ($category in $response.categories) { + Write-Host "Deleting category: $($category.name) (ID: $($category.id))" + $deleteUrl = "$Z4JUrl/api/v2/help_center/categories/$($category.id)" + try { + Invoke-RestMethod -Uri $deleteUrl -Method Delete -Headers $headers + Write-Host "Successfully deleted category: $($category.name)" + } + catch { + Write-Error "Error deleting category with ID $($category.id): $_" + } + } + } + + $nextPage = $response.next_page + } + catch { + Write-Error "Error fetching categories: $_" + exit 1 + } +} while ($null -ne $nextPage) + +Write-Host "Category cleanup complete." \ No newline at end of file diff --git a/cleanup_user_segments.ps1 b/cleanup_user_segments.ps1 index 568b07a..4f7b908 100644 --- a/cleanup_user_segments.ps1 +++ b/cleanup_user_segments.ps1 @@ -5,9 +5,9 @@ This script paginates through all pages of the GET /api/v2/help_center/user_segments endpoint and deletes any user segment that is not built-in. .DESCRIPTION This script will first check for the presence of the following environment variables: - - ZENDESK_URL: The URL of your Zendesk instance (e.g., https://your-subdomain.zendesk.com) - - ZENDESK_API_TOKEN: Your Zendesk API token. - - ZENDESK_ADMIN_EMAIL: The email address of a Zendesk admin. + - Z4J_URL: The URL of your Zendesk instance (e.g., https://your-subdomain.zendesk.com) + - Z4J_TOKEN: Your Zendesk API token. + - Z4J_ADMIN_EMAIL: The email address of a Zendesk admin. If all environment variables are present, it will then: 1. Paginate through all user segments. @@ -21,7 +21,7 @@ param() #region Environment Variable Validation -$requiredEnvVars = @("ZENDESK_URL", "ZENDESK_API_TOKEN", "ZENDESK_ADMIN_EMAIL") +$requiredEnvVars = @("Z4J_URL", "Z4J_TOKEN", "Z4J_ADMIN_EMAIL") $missingEnvVars = @() foreach ($var in $requiredEnvVars) { @@ -36,9 +36,9 @@ if ($missingEnvVars.Count -gt 0) { } #endregion -$zendeskUrl = $env:ZENDESK_URL -$apiToken = $env:ZENDESK_API_TOKEN -$adminEmail = $env:ZENDESK_ADMIN_EMAIL +$Z4JUrl = $env:Z4J_URL +$apiToken = $env:Z4J_TOKEN +$adminEmail = $env:Z4J_ADMIN_EMAIL $credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${adminEmail}/token:${apiToken}")) $headers = @{ @@ -46,7 +46,7 @@ $headers = @{ "Content-Type" = "application/json" } -$nextPage = "$zendeskUrl/api/v2/help_center/user_segments" +$nextPage = "$Z4JUrl/api/v2/help_center/user_segments" do { try { @@ -57,7 +57,7 @@ do { foreach ($segment in $response.user_segments) { if ($segment.built_in -eq $false) { Write-Host "Deleting user segment: $($segment.name) (ID: $($segment.id))" - $deleteUrl = "$zendeskUrl/api/v2/help_center/user_segments/$($segment.id)" + $deleteUrl = "$Z4JUrl/api/v2/help_center/user_segments/$($segment.id)" try { Invoke-RestMethod -Uri $deleteUrl -Method Delete -Headers $headers Write-Host "Successfully deleted user segment: $($segment.name)" diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index ec029e8..7de98a7 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -14,7 +14,7 @@ paths:

Returns a list of all system and custom ticket fields in your account.

For end users, only the ticket fields with visible_in_portal set to true are returned.

Consider caching this resource to use with the{@link TicketClient}.

-

Allowed For

+

lookup relationships) to + href="/api-reference/ticketing/lookup_relationships/lookup_relationships/'>lookup relationships) to another object such as a user, ticket, or organization

@@ -114,13 +114,13 @@ paths:

Note: Tags can't be re-used across custom ticket fields. For example, if you configure a tag for a checkbox field, you can't use that tag value for a dropdown (tagger) field option. The use of tags isn't validated and can prevent editing in the future.

-

See About custom field types in the Zendesk +

See About custom field types in the Zendesk Help Center.

-

Allowed For

+

Field limits

+

List Comments

-

Allowed For

+

Returns a number of ticket properties though not the ticket comments. To get the comments, use List Comments

+

Pagination.

-

Allowed for

+

This endpoint supports pagination as described in Pagination.

+

Pagination

+

Pagination.

+

See Pagination.

responses: "200": description: OK Response @@ -559,7 +559,7 @@ paths: summary: Create Category by Locale description: |-

You must specify a category name and locale. The locale can be omitted if it's specified - in the URL. Optionally, you can specify multiple translations for + in the URL. Optionally, you can specify multiple translations for the category. The specified locales must be enabled for the current Help Center.

Allowed for

    @@ -623,6 +623,14 @@ paths:
    • Help Center managers
    + requestBody: + content: + application/json: + schema: + type: object + properties: + category: + $ref: '#/components/schemas/Category' responses: "200": description: OK Response @@ -708,13 +716,21 @@ paths: - Category summary: Create Category description: |- -

    You must specify a category name and locale. The locale can be omitted if it's specified - in the URL. Optionally, you can specify multiple translations for +

    You must specify a category name and locale. + Optionally, you can specify multiple translations for the category. The specified locales must be enabled for the current Help Center.

    Allowed for

    • Help Center managers
    + requestBody: + content: + application/json: + schema: + type: object + properties: + category: + $ref: '#/components/schemas/Category' responses: "201": description: OK Response @@ -738,10 +754,13 @@ paths: - Category summary: Show Category description: |- +

    Note: {/locale} is an optional parameter for admins and agents. End users and anonymous users must provide the parameter.

    Allowed for

    • Agents
    +

    Translations are embedded within the category because they're + not shared between resources.

    responses: "200": description:

    description

    @@ -761,6 +780,14 @@ paths:
    • Help Center managers
    + requestBody: + content: + application/json: + schema: + type: object + properties: + category: + $ref: '#/components/schemas/Category' responses: "200": description: OK Response @@ -808,7 +835,7 @@ paths: parameters: - name: query in: query - description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. required: true schema: type: string @@ -852,7 +879,7 @@ paths: parameters: - name: query in: query - description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. required: true schema: type: string @@ -902,7 +929,7 @@ paths: parameters: - name: query in: query - description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. required: true schema: type: string @@ -980,7 +1007,7 @@ components: Attachment: type: object description: | - A file represented as an Attachment object + A file represented as an Attachment object allOf: - $ref: '#/components/schemas/AttachmentBase' - $ref: '#/components/schemas/AttachmentThumbnails' @@ -994,7 +1021,7 @@ components: content_url: type: string description: | - A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties + A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties readOnly: true deleted: type: boolean @@ -1148,7 +1175,7 @@ components: type: string description: |

    HTML body of the article. Unsafe tags and attributes may be removed before display. For a list of safe tags and attributes, - see Allowing unsafe HTML in Help Center articles in Zendesk help

    + see Allowing unsafe HTML in Help Center articles in Zendesk help

    comments_disabled: type: boolean description: True if comments are disabled; false otherwise @@ -1398,39 +1425,36 @@ components: html_url: type: string description: The url of this category in Help Center - # readOnly: true id: type: integer format: int64 description: Automatically assigned when creating categories - # readOnly: true locale: - type: string - description: The locale where the category is displayed + $ref: '#/components/schemas/LocaleAbbreviation' name: type: string description: The name of the category outdated: type: boolean description: Whether the category is out of date - # readOnly: true position: type: integer format: int64 description: The position of this category relative to other categories source_locale: - type: string - description: The source (default) locale of the category - # readOnly: true + $ref: '#/components/schemas/LocaleAbbreviation' + translations: + type: array + description: The translations for the category + items: + $ref: '#/components/schemas/Translation' updated_at: type: string format: date-time description: The time at which the category was last updated - # readOnly: true url: type: string description: The API url of this category - # readOnly: true required: - name CategoryResponse: @@ -1455,13 +1479,13 @@ components: author_id: type: integer format: int64 - description: The id of the author of this comment. Writable on create by Help Center managers. See Create Comment + description: The id of the author of this comment. Writable on create by Help Center managers. See Create Comment body: type: string - description: The comment made by the author. See User content + description: The comment made by the author. See User content created_at: type: string - description: The time the comment was created. Writable on create by Help Center managers. See Create Comment + description: The time the comment was created. Writable on create by Help Center managers. See Create Comment html_url: type: string description: The url at which the comment is presented in Help Center @@ -1572,7 +1596,7 @@ components: readOnly: true source_locale: type: string - description: Used only for Create Section Subscription and Create Article Subscription, where it's mandatory. Selects the locale of the content to be subscribed + description: Used only for Create Section Subscription and Create Article Subscription, where it's mandatory. Selects the locale of the content to be subscribed updated_at: type: string description: The time at which the subscription was last updated @@ -1864,6 +1888,83 @@ components: name: English updated_at: "2011-05-05T10:38:52Z" url: https://company.zendesk.com/api/v2/locales/en-US.json + LocaleAbbreviation: + type: string + description: The locale of the translation + enum: + - ar + - pt-br + - bg + - cs + - da + - nl + - en-gb + - en-us + - fa-af + - fil + - fi + - fr + - fr-ca + - de + - el + - he + - hi + - hu + - id + - it + - ja + - ko + - ms + - no + - pl + - ro + - ru + - zh-cn + - es + - sk + - sv + - th + - zh-tw + - tr + - uk + - vi + x-enum-varnames: + - ARABIC + - PORTUGUESE_BRAZIL + - BULGARIAN + - CZECH + - DANISH + - DUTCH + - ENGLISH_UNITED_KINGDOM + - ENGLISH_UNITED_STATES + - DARI_PERSIAN_AFGHANISTAN + - FILIPINO + - FINNISH + - FRENCH + - FRENCH_CANADA + - GERMAN + - GREEK + - HEBREW + - HINDI + - HUNGARIAN + - INDONESIAN + - ITALIAN + - JAPANESE + - KOREAN + - MALAY + - NORWEGIAN + - POLISH + - ROMANIAN + - RUSSIAN + - SIMPLIFIED_CHINESE + - SPANISH + - SLOVAK + - SWEDISH + - THAI + - TRADITIONAL_CHINESE + - TURKISH + - UKRAINIAN + - VIETNAMESE LocaleResponse: type: object properties: @@ -1891,13 +1992,13 @@ components: author_id: type: integer format: int64 - description: The id of the author of the comment. Writable on create by Help Center managers. See Create Post Comment + description: The id of the author of the comment. Writable on create by Help Center managers. See Create Post Comment body: type: string - description: The comment made by the author. See User content + description: The comment made by the author. See User content created_at: type: string - description: When the comment was created. Writable on create by Help Center managers. See Create Post Comment + description: When the comment was created. Writable on create by Help Center managers. See Create Post Comment html_url: type: string description: The community url of the comment @@ -1970,7 +2071,7 @@ components: author_id: type: integer format: int64 - description: The id of the author of the post. *Writable on create by Help Center managers -- see Create Post + description: The id of the author of the post. *Writable on create by Help Center managers -- see Create Post readOnly: true closed: type: boolean @@ -1989,11 +2090,11 @@ components: created_at: type: string format: date-time - description: When the post was created. Writable on create by Help Center managers -- see Create Post + description: When the post was created. Writable on create by Help Center managers -- see Create Post readOnly: true details: type: string - description: The details of the post made by the author. See User content + description: The details of the post made by the author. See User content featured: type: boolean description: Whether the post is featured @@ -2259,7 +2360,7 @@ components: brand_id: type: integer format: int64 - description: The id of the brand this ticket is associated with. See Setting up multiple brands + description: The id of the brand this ticket is associated with. See Setting up multiple brands collaborator_ids: type: array description: The ids of users currently CC'ed on the ticket @@ -2268,12 +2369,12 @@ components: format: int64 collaborators: type: array - description: POST requests only. Users to add as cc's when creating a ticket. See Setting Collaborators + description: POST requests only. Users to add as cc's when creating a ticket. See Setting Collaborators items: $ref: '#/components/schemas/Collaborator' comment: type: object - description: Write only. An object that adds a comment to the ticket. See Ticket comments. To include an attachment with the comment, see Attaching files. A ticket can contain up to 5000 comments in total, including both public and private comments. Once this limit is reached, any additional attempts to add comments results in a 422 error. The ticket can still be updated in other ways, provided that no new comments are added. + description: Write only. An object that adds a comment to the ticket. See Ticket comments. To include an attachment with the comment, see Attaching files. A ticket can contain up to 5000 comments in total, including both public and private comments. Once this limit is reached, any additional attempts to add comments results in a 422 error. The ticket can still be updated in other ways, provided that no new comments are added. writeOnly: true created_at: type: string @@ -2282,7 +2383,7 @@ components: readOnly: true custom_fields: type: array - description: Custom fields for the ticket. See Setting custom field values + description: Custom fields for the ticket. See Setting custom field values items: type: object properties: @@ -2296,39 +2397,39 @@ components: custom_status_id: type: integer format: int64 - description: The custom ticket status id of the ticket. See custom ticket statuses + description: The custom ticket status id of the ticket. See custom ticket statuses description: type: string description: | - Read-only first comment on the ticket. When creating a ticket, use comment to set the description. See Description and first comment + Read-only first comment on the ticket. When creating a ticket, use comment to set the description. See Description and first comment readOnly: true due_at: type: string format: date-time - description: If this is a ticket of type "task" it has a due date. Due date format uses ISO 8601 format + description: If this is a ticket of type "task" it has a due date. Due date format uses ISO 8601 format nullable: true email_cc_ids: type: array - description: The ids of agents or end users currently CC'ed on the ticket. Ignored when CCs and followers is not enabled + description: The ids of agents or end users currently CC'ed on the ticket. Ignored when CCs and followers is not enabled items: type: integer format: int64 email_ccs: type: object - description: Write only. An array of objects that represents agent or end users email CCs to add or delete from the ticket. See Setting email CCs. Ignored when CCs and followers is not enabled + description: Write only. An array of objects that represents agent or end users email CCs to add or delete from the ticket. See Setting email CCs. Ignored when CCs and followers is not enabled writeOnly: true external_id: type: string description: An id you can use to link Zendesk Support tickets to local records follower_ids: type: array - description: The ids of agents currently following the ticket. Ignored when CCs and followers is not enabled + description: The ids of agents currently following the ticket. Ignored when CCs and followers is not enabled items: type: integer format: int64 followers: type: object - description: Write only. An array of objects that represents agent followers to add or delete from the ticket. See Setting followers. Ignored when CCs and followers is not enabled + description: Write only. An array of objects that represents agent followers to add or delete from the ticket. See Setting followers. Ignored when CCs and followers is not enabled writeOnly: true followup_ids: type: array @@ -2344,7 +2445,7 @@ components: readOnly: true from_messaging_channel: type: boolean - description: If true, the ticket's via type is a messaging channel. + description: If true, the ticket's via type is a messaging channel. readOnly: true generated_timestamp: type: integer @@ -2381,12 +2482,12 @@ components: format: int64 metadata: type: object - description: Write only. Metadata for the audit. In the audit object, the data is specified in the custom property of the metadata object. See Setting Metadata + description: Write only. Metadata for the audit. In the audit object, the data is specified in the custom property of the metadata object. See Setting Metadata writeOnly: true organization_id: type: integer format: int64 - description: The organization of the requester. You can only specify the ID of an organization associated with the requester. See Organization Memberships + description: The organization of the requester. You can only specify the ID of an organization associated with the requester. See Organization Memberships priority: type: string description: The urgency with which the ticket should be addressed @@ -2402,13 +2503,13 @@ components: raw_subject: type: string description: | - The dynamic content placeholder, if present, or the "subject" value, if not. See Dynamic Content Items + The dynamic content placeholder, if present, or the "subject" value, if not. See Dynamic Content Items recipient: type: string description: The original recipient e-mail address of the ticket. Notification emails for the ticket are sent from this address requester: type: object - description: Write only. See Creating a ticket with a new requester + description: Write only. See Creating a ticket with a new requester writeOnly: true requester_id: type: integer @@ -2416,7 +2517,7 @@ components: description: The user who requested this ticket safe_update: type: boolean - description: Write only. Optional boolean. When true and an update_stamp date is included, protects against ticket update collisions and returns a message to let you know if one occurs. See Protecting against ticket update collisions. A value of false has the same effect as true. Omit the property to force the updates to not be safe + description: Write only. Optional boolean. When true and an update_stamp date is included, protects against ticket update collisions and returns a message to let you know if one occurs. See Protecting against ticket update collisions. A value of false has the same effect as true. Omit the property to force the updates to not be safe writeOnly: true satisfaction_rating: type: object @@ -2435,7 +2536,7 @@ components: The state of the ticket. If your account has activated custom ticket statuses, this is the ticket's - status category. See custom ticket statuses + status category. See custom ticket statuses enum: - new - open @@ -2446,14 +2547,14 @@ components: subject: type: string description: | - The value of the subject field for this ticket. See Subject + The value of the subject field for this ticket. See Subject submitter_id: type: integer format: int64 description: The user who submitted the ticket. The submitter always becomes the author of the first comment on the ticket tags: type: array - description: The array of tags applied to this ticket. Unless otherwise specified, the set tag behavior is used, which overwrites and replaces existing tags + description: The array of tags applied to this ticket. Unless otherwise specified, the set tag behavior is used, which overwrites and replaces existing tags items: type: string ticket_form_id: @@ -2471,7 +2572,7 @@ components: updated_at: type: string format: date-time - description: When this record last got updated. It is updated only if the update generates a ticket event + description: When this record last got updated. It is updated only if the update generates a ticket event readOnly: true updated_stamp: type: string @@ -2483,7 +2584,7 @@ components: readOnly: true via: type: object - description: For more information, see the Via object reference + description: For more information, see the Via object reference properties: channel: type: string @@ -2497,15 +2598,15 @@ components: via_followup_source_id: type: integer format: int64 - description: POST requests only. The id of a closed ticket when creating a follow-up ticket. See Creating a follow-up ticket + description: POST requests only. The id of a closed ticket when creating a follow-up ticket. See Creating a follow-up ticket via_id: type: integer format: int64 - description: Write only. For more information, see the Via object reference + description: Write only. For more information, see the Via object reference writeOnly: true voice_comment: type: object - description: Write only. See Creating voicemail ticket + description: Write only. See Creating voicemail ticket writeOnly: true example: assignee_id: 235323 @@ -2579,18 +2680,18 @@ components: description: Enterprise only. The id of the brand this ticket is associated with collaborators: type: array - description: POST requests only. Users to add as cc's when creating a ticket. See Setting Collaborators + description: POST requests only. Users to add as cc's when creating a ticket. See Setting Collaborators items: $ref: '#/components/schemas/Collaborator' email_cc_ids: type: array - description: The ids of agents or end users currently CC'ed on the ticket. See CCs and followers resources in the Support Help Center + description: The ids of agents or end users currently CC'ed on the ticket. See CCs and followers resources in the Support Help Center items: type: integer format: int64 follower_ids: type: array - description: The ids of agents currently following the ticket. See CCs and followers resources + description: The ids of agents currently following the ticket. See CCs and followers resources items: type: integer format: int64 @@ -2603,7 +2704,7 @@ components: raw_subject: type: string description: | - The dynamic content placeholder, if present, or the "subject" value, if not. See Dynamic Content Items + The dynamic content placeholder, if present, or the "subject" value, if not. See Dynamic Content Items recipient: type: string description: The original recipient e-mail address of the ticket @@ -2620,7 +2721,7 @@ components: via_followup_source_id: type: integer format: int64 - description: POST requests only. The id of a closed ticket when creating a follow-up ticket. See Creating a follow-up ticket + description: POST requests only. The id of a closed ticket when creating a follow-up ticket. See Creating a follow-up ticket required: - comment example: @@ -2700,13 +2801,13 @@ components: description: The relative position of the ticket field on a ticket. Note that for accounts with ticket forms, positions are controlled by the different forms raw_description: type: string - description: The dynamic content placeholder if present, or the description value if not. See Dynamic Content + description: The dynamic content placeholder if present, or the description value if not. See Dynamic Content raw_title: type: string - description: The dynamic content placeholder if present, or the title value if not. See Dynamic Content + description: The dynamic content placeholder if present, or the title value if not. See Dynamic Content raw_title_in_portal: type: string - description: The dynamic content placeholder if present, or the "title_in_portal" value if not. See Dynamic Content + description: The dynamic content placeholder if present, or the "title_in_portal" value if not. See Dynamic Content regexp_for_validation: type: string description: For "regexp" fields only. The validation pattern for a field value to be deemed valid @@ -2748,7 +2849,7 @@ components: description: The title of the ticket field for end users in Help Center type: type: string - description: System or custom field type. Editable for custom field types and only on creation. See Create Ticket Field + description: System or custom field type. Editable for custom field types and only on creation. See Create Ticket Field updated_at: type: string format: date-time @@ -2788,7 +2889,7 @@ components: properties: additional_collaborators: type: array - description: An array of numeric IDs, emails, or objects containing name and email properties. See Setting Collaborators. An email notification is sent to them when the ticket is updated + description: An array of numeric IDs, emails, or objects containing name and email properties. See Setting Collaborators. An email notification is sent to them when the ticket is updated items: $ref: '#/components/schemas/Collaborator' assignee_email: @@ -2812,20 +2913,20 @@ components: $ref: '#/components/schemas/TicketComment' custom_fields: type: array - description: Custom fields for the ticket. See Setting custom field values + description: Custom fields for the ticket. See Setting custom field values items: $ref: '#/components/schemas/CustomField' custom_status_id: type: integer - description: The custom ticket status id of the ticket. See custom ticket statuses + description: The custom ticket status id of the ticket. See custom ticket statuses due_at: type: string format: date-time - description: If this is a ticket of type "task" it has a due date. Due date format uses ISO 8601 format. + description: If this is a ticket of type "task" it has a due date. Due date format uses ISO 8601 format. nullable: true email_ccs: type: array - description: An array of objects that represent agent or end users email CCs to add or delete from the ticket. See Setting email CCs + description: An array of objects that represent agent or end users email CCs to add or delete from the ticket. See Setting email CCs items: $ref: '#/components/schemas/EmailCC' external_id: @@ -2833,7 +2934,7 @@ components: description: An id you can use to link Zendesk Support tickets to local records followers: type: array - description: An array of objects that represent agent followers to add or delete from the ticket. See Setting followers + description: An array of objects that represent agent followers to add or delete from the ticket. See Setting followers items: $ref: '#/components/schemas/Follower' group_id: @@ -2841,7 +2942,7 @@ components: description: The group this ticket is assigned to organization_id: type: integer - description: The organization of the requester. You can only specify the ID of an organization associated with the requester. See Organization Memberships + description: The organization of the requester. You can only specify the ID of an organization associated with the requester. See Organization Memberships priority: type: string description: The urgency with which the ticket should be addressed. @@ -2870,7 +2971,7 @@ components: The state of the ticket. If your account has activated custom ticket statuses, this is the ticket's - status category. See custom ticket statuses. + status category. See custom ticket statuses. enum: - new - open @@ -2917,7 +3018,7 @@ components: $ref: '#/components/schemas/TicketUpdateInput' TicketAuditVia: type: object - description: Describes how the object was created. See the Via object reference + description: Describes how the object was created. See the Via object reference properties: channel: type: string @@ -2933,20 +3034,20 @@ components: properties: attachments: type: array - description: Attachments, if any. See Attachment + description: Attachments, if any. See Attachment items: $ref: '#/components/schemas/Attachment' readOnly: true audit_id: type: integer - description: The id of the ticket audit record. See Show Audit + description: The id of the ticket audit record. See Show Audit readOnly: true author_id: type: integer - description: The id of the comment author. See Author id + description: The id of the comment author. See Author id body: type: string - description: The comment string. See Bodies + description: The comment string. See Bodies created_at: type: string format: date-time @@ -2954,30 +3055,30 @@ components: readOnly: true html_body: type: string - description: The comment formatted as HTML. See Bodies + description: The comment formatted as HTML. See Bodies id: type: integer description: Automatically assigned when the comment is created readOnly: true metadata: type: object - description: System information (web client, IP address, etc.) and comment flags, if any. See Comment flags + description: System information (web client, IP address, etc.) and comment flags, if any. See Comment flags additionalProperties: true readOnly: true plain_body: type: string - description: The comment presented as plain text. See Bodies + description: The comment presented as plain text. See Bodies readOnly: true public: type: boolean description: true if a public comment; false if an internal note. The initial value set on ticket creation persists for any additional comment unless you change it type: type: string - description: '`Comment` or `VoiceComment`. The JSON object for adding voice comments to tickets is different. See Adding voice comments to tickets' + description: "`Comment` or `VoiceComment`. The JSON object for adding voice comments to tickets is different. See Adding voice comments to tickets" readOnly: true uploads: type: array - description: List of tokens received from uploading files for comment attachments. The files are attached by creating or updating tickets with the tokens. See Attaching files in Tickets + description: List of tokens received from uploading files for comment attachments. The files are attached by creating or updating tickets with the tokens. See Attaching files in Tickets items: type: string via: @@ -3130,8 +3231,7 @@ components: description: Automatically assigned when a translation is created readOnly: true locale: - type: string - description: The locale of the translation + $ref: '#/components/schemas/LocaleAbbreviation' outdated: type: boolean description: True if the translation is outdated; false otherwise. False by default @@ -3161,12 +3261,6 @@ components: type: string description: The API url of the translation readOnly: true - example: - id: 3243452 - locale: en - source_id: 768934 - source_type: Article - title: Hello translation required: - locale - title @@ -3357,7 +3451,7 @@ components: Via: type: object description: | - An object explaining how the ticket was created. See the Via object reference + An object explaining how the ticket was created. See the Via object reference properties: channel: type: string @@ -3584,9 +3678,7 @@ components: description: The locale the item is displayed in. (must be lowercase, even if returned from zendesk as mixed case) required: true schema: - type: string - example: en-us - example: en-us + $ref: '#/components/schemas/LocaleAbbreviation' PostCommentId: name: post_comment_id in: path diff --git a/src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy b/src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy index eb59fe7..84d5ea4 100644 --- a/src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy @@ -10,6 +10,7 @@ import spock.lang.Shared import spock.lang.Specification @MicronautTest +@SuppressWarnings("GroovyAssignabilityCheck") class Z4jSpec extends Specification { @Shared diff --git a/src/test/groovy/lol/pbu/z4j/client/ArticleClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/ArticleClientSpec.groovy index 4f8728f..24829b7 100644 --- a/src/test/groovy/lol/pbu/z4j/client/ArticleClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/ArticleClientSpec.groovy @@ -5,6 +5,7 @@ import lol.pbu.z4j.Z4jSpec import lol.pbu.z4j.model.ArticlesResponse import lol.pbu.z4j.model.ListArticlesSortByParameter import lol.pbu.z4j.model.ListArticlesSortOrderParameter +import lol.pbu.z4j.model.LocaleAbbreviation import reactor.core.publisher.Mono import spock.lang.Shared @@ -15,16 +16,16 @@ class ArticleClientSpec extends Z4jSpec { ArticleClient adminArticleClient, agentArticleClient, userArticleClient @Shared - List allLocales + List allLocales def setupSpec() { adminArticleClient = adminCtx.getBean(ArticleClient.class) agentArticleClient = agentCtx.getBean(ArticleClient.class) userArticleClient = userCtx.getBean(ArticleClient.class) - allLocales = userCtx.getBean(LocaleClient.class).listLocales().block().locales.collect { it.locale.toLowerCase() } + allLocales = List.of(LocaleAbbreviation.ENGLISH_UNITED_STATES, LocaleAbbreviation.FRENCH) } - def "can use ListArticles for other tests using the '#locale' locale"(ArticleClient articleClient, String locale, ListArticlesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder, Long startTime, String labelNames) { + def "can use ListArticles for other tests using the '#locale' locale"(ArticleClient articleClient, LocaleAbbreviation locale, ListArticlesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder, Long startTime, String labelNames) { // https://github.com/PeanutButter-Unicorn/z4j/issues/31 when: "query articles list for the '#locale' locale" Mono response = articleClient.listArticles(locale, sortBy, sortOrder, startTime, labelNames) diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy index 3a4d33c..11052ad 100644 --- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy @@ -1,15 +1,16 @@ package lol.pbu.z4j.client - import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.extensions.spock.annotation.MicronautTest import lol.pbu.z4j.Z4jSpec import lol.pbu.z4j.model.* import spock.lang.Shared +import static io.micronaut.http.HttpStatus.BAD_REQUEST import static io.micronaut.http.HttpStatus.FORBIDDEN @MicronautTest +@SuppressWarnings("GroovyAssignabilityCheck") class CategoryClientSpec extends Z4jSpec { @Shared @@ -19,19 +20,19 @@ class CategoryClientSpec extends Z4jSpec { List userSegments @Shared - List allLocales + List allLocales def setupSpec() { adminCategoryClient = adminCtx.getBean(CategoryClient.class) agentCategoryClient = agentCtx.getBean(CategoryClient.class) userCategoryClient = userCtx.getBean(CategoryClient.class) - allLocales = adminCtx.getBean(LocaleClient.class).listLocales().block().locales.collect { it.locale.toLowerCase() } + allLocales = List.of(LocaleAbbreviation.ENGLISH_UNITED_STATES, LocaleAbbreviation.FRENCH) userSegments = adminCtx.getBean(UserSegmentClient.class).listUserSegments(null).block().getUserSegments() assert userSegments.size() >= 2 // built in segments should be at least 2, this is here to just double check this doesn't change } - def "can use ListArticles using the '#locale' locale for the #userType user type"(CategoryClient categoryClient, String userType, String locale, ListCategoriesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder) { + def "can use ListArticles using the '#locale' locale for the #userType user type"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale, ListCategoriesSortByParameter sortBy, ListArticlesSortOrderParameter sortOrder) { when: "query Categories list for the '#locale' locale" categoryClient.listCategories(locale, sortBy, sortOrder).block() @@ -60,7 +61,7 @@ class CategoryClientSpec extends Z4jSpec { ].combinations() } - def "can use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + def "can use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale) { given: CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() String categoryName = faker.animal().name() @@ -80,7 +81,40 @@ class CategoryClientSpec extends Z4jSpec { where: [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations() } - def "cannot use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + + def "can use CreateCategoryNoLocale as an #userType and update it with a translation"(CategoryClient categoryClient, String userType) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + def translations = List.of(new Translation(LocaleAbbreviation.FRENCH, faker.backToTheFuture().quote()), + new Translation(LocaleAbbreviation.ENGLISH_UNITED_STATES, faker.redDeadRedemption2().quote())) + Category category = new Category(categoryName) + .setLocale(LocaleAbbreviation.ENGLISH_UNITED_STATES) + .setPosition(0) + createCategoryRequest.setCategory(category) + + + when: + CategoryResponse response = categoryClient.createCategoryNoLocale(createCategoryRequest).block() + + and: + category.setTranslations(translations) + categoryClient.updateCategoryNoLocale(response.getCategory().getId(), new CreateCategoryRequest(category)).block() + + then: + noExceptionThrown() + + cleanup: + categoryClient.deleteCategory(LocaleAbbreviation.ENGLISH_UNITED_STATES, response.category.id) + + where: + [[categoryClient, userType]] << [[[adminCategoryClient, "admin"]]].combinations() + + + } + + + def "cannot use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale) { given: CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() String categoryName = faker.animal().name() @@ -100,13 +134,35 @@ class CategoryClientSpec extends Z4jSpec { cleanup: "deleting #categoryName from the #locale locale" try { adminCategoryClient.deleteCategory(locale, response.getCategory().getId()) - } catch (NullPointerException ignored) {} + } catch (NullPointerException ignored) { + } where: [[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations() } - def "can use DeleteCategory as an #userType for the '#locale"(CategoryClient categoryClient, String userType, String locale) { + def "cannot use CreateCategoryNoLocale as an #userType"(CategoryClient categoryClient, String userType) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + + when: "category name to be created is #categoryName" + categoryClient.createCategoryNoLocale(createCategoryRequest).block() + + then: + HttpClientResponseException error = thrown(HttpClientResponseException) + + and: + error.getStatus() == FORBIDDEN + + where: + [[categoryClient, userType]] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]]].combinations() + } + + def "can use DeleteCategory as an #userType for the '#locale"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale) { given: CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() String categoryName = faker.bluey().quote() @@ -125,10 +181,10 @@ class CategoryClientSpec extends Z4jSpec { [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations() } - def "cannot use DeleteCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) { + def "cannot use DeleteCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale) { given: CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() - String categoryName = faker.bluey().quote() + " " + UUID.randomUUID().toString() + String categoryName = faker.bluey().quote() + " " + UUID.randomUUID().toString() Category category = new Category(categoryName) category.setDescription(faker.lordOfTheRings().location()) createCategoryRequest.setCategory(category) @@ -144,11 +200,79 @@ class CategoryClientSpec extends Z4jSpec { // error.getStatus() == FORBIDDEN cleanup: - try { - adminCategoryClient.deleteCategory(locale, response.getCategory().getId()) - }catch (NullPointerException ignored){} + adminCategoryClient.deleteCategory(locale, response.getCategory().getId()) + where: [[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations() } + + def "can use ShowCategory as a #userType for the #locale locale"(CategoryClient categoryClient, String userType, LocaleAbbreviation locale) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + CategoryResponse createdCategory = adminCategoryClient.createCategory(locale, createCategoryRequest).block() + + when: "showing category #categoryName" + categoryClient.showCategory(locale, createdCategory.getCategory().getId()).block() + + then: + noExceptionThrown() + + cleanup: + adminCategoryClient.deleteCategory(locale, createdCategory.getCategory().getId()).block() + + where: + [[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"], [agentCategoryClient, "agent"], [userCategoryClient, "user"]], allLocales].combinations() + } + + def "can use ShowCategoryNoLocale as a #userType"(CategoryClient categoryClient, String userType) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + CategoryResponse createdCategory = adminCategoryClient.createCategoryNoLocale(createCategoryRequest).block() + + when: "showing category #categoryName" + categoryClient.showCategoryNoLocale(createdCategory.getCategory().getId()).block() + + then: + noExceptionThrown() + + cleanup: + adminCategoryClient.deleteCategory(LocaleAbbreviation.ENGLISH_UNITED_STATES, createdCategory.getCategory().getId()).block() + + where: + [[categoryClient, userType]] << [[[adminCategoryClient, "admin"], [agentCategoryClient, "agent"]]].combinations() + } + + def "cannot use ShowCategoryNoLocale as a #userType"(CategoryClient categoryClient, String userType) { + given: + CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest() + String categoryName = faker.animal().name() + Category category = new Category(categoryName) + category.setDescription(faker.backToTheFuture().quote()) + createCategoryRequest.setCategory(category) + CategoryResponse createdCategory = adminCategoryClient.createCategoryNoLocale(createCategoryRequest).block() + + when: "showing category #categoryName" + categoryClient.showCategoryNoLocale(createdCategory.getCategory().getId()).block() + + then: + HttpClientResponseException error = thrown(HttpClientResponseException) + + and: + error.getStatus() == BAD_REQUEST + + cleanup: + adminCategoryClient.deleteCategory(LocaleAbbreviation.ENGLISH_UNITED_STATES, createdCategory.getCategory().getId()).block() + + where: + [[categoryClient, userType]] << [[[userCategoryClient, "user"]]].combinations() + } } diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index b0f12e7..865f885 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -71,6 +71,7 @@ class SearchClientSpec extends Z4jSpec { userSearchClient | _ } + @SuppressWarnings("GroovyAssignabilityCheck") void "an #clientName can call export method with pageSize: #pageSize, pageAfter: #pageAfter, filterType: #filterType and include: #include"(String clientName, SearchClient client, int pageSize, String pageAfter, SearchExportType filterType, String include) { when: client.export(faker.bluey().quote(), pageSize, pageAfter, filterType, include).block() diff --git a/src/test/groovy/lol/pbu/z4j/client/TicketClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/TicketClientSpec.groovy index 856bd6d..caf86f0 100644 --- a/src/test/groovy/lol/pbu/z4j/client/TicketClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/TicketClientSpec.groovy @@ -171,11 +171,6 @@ class TicketClientSpec extends Z4jSpec { then: noExceptionThrown() - and: - if (creator) { - //todo: https://github.com/PeanutButter-Unicorn/z4j/issues/52 - } - where: [[client, clientType, ignored, alsoIgnored], creator, locale] << [ clientTestMatrix.findAll { it.shouldSucceed || it.clientType == "simple user" }, [true, false, null], accountLocales diff --git a/src/test/groovy/lol/pbu/z4j/model/TranslationsResponseSpec.groovy b/src/test/groovy/lol/pbu/z4j/model/TranslationsResponseSpec.groovy index ec7571a..bceaa55 100644 --- a/src/test/groovy/lol/pbu/z4j/model/TranslationsResponseSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/model/TranslationsResponseSpec.groovy @@ -6,26 +6,33 @@ import spock.lang.Unroll class TranslationsResponseSpec extends Z4jSpec { @Unroll - def "should add translations item"() { + def "should add translations item"(LocaleAbbreviation locale) { given: def translationsResponse = new TranslationsResponse() translationsResponse.translations == null - def translation = new Translation(faker.lorem().word(), faker.lorem().sentence()) + def translation = new Translation(locale, faker.lorem().sentence()) when: translationsResponse.addTranslationsItem(translation) then: translationsResponse.translations.size() == 1 - translationsResponse.translations.getAt(0) == translation + translationsResponse.translations[0] == translation + + where: + locale << LocaleAbbreviation.values() } @Unroll - def "add translations item to existing list"() { + def "add translations item to existing list"(LocaleAbbreviation locale) { given: - def existingTranslation = new Translation(faker.lorem().word(), faker.lorem().sentence()) + def existingTranslation = new Translation(locale, faker.lorem().sentence()) def translationsResponse = new TranslationsResponse(translations: [existingTranslation]) - def newTranslation = new Translation(faker.lorem().word(), faker.lorem().sentence()) + LocaleAbbreviation update = LocaleAbbreviation.HEBREW + if (locale == update) { + update = LocaleAbbreviation.SIMPLIFIED_CHINESE + } + def newTranslation = new Translation(update, faker.lorem().sentence()) when: translationsResponse.addTranslationsItem(newTranslation) @@ -33,5 +40,8 @@ class TranslationsResponseSpec extends Z4jSpec { then: translationsResponse.translations.size() == 2 translationsResponse.translations.containsAll([existingTranslation, newTranslation]) + + where: + locale << LocaleAbbreviation.values() } } diff --git a/src/test/resources/Application-test.yaml b/src/test/resources/Application-test.yaml new file mode 100644 index 0000000..2946078 --- /dev/null +++ b/src/test/resources/Application-test.yaml @@ -0,0 +1,8 @@ +logger: + levels: + io.micronaut.http.client: WARN +#Z4J_ADMIN_EMAIL: +#Z4J_TOKEN: +#Z4J_AGENT_EMAIL: +#Z4J_END_USER_EMAIL: +#Z4J_URL: \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..ddda692 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + \ No newline at end of file From 800d0c1e4388058f6aa9708b75064c53a90964bd Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Mon, 20 Apr 2026 22:21:18 -0600 Subject: [PATCH 31/31] chore: set project version to 0.1.1 --- README.md | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb4122f..f0213f1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To get a local copy up and running, please follow the detailed setup instruction | Gradle Kotlin | Gradle | |:-----------------------------------------------|:----------------------------------------------| -|
    implementation("lol.pbu:z4j:0.1.0")
    |
    implementation 'lol.pbu:z4j:0.1.0'
    | +|
    implementation("lol.pbu:z4j:0.1.1")
    |
    implementation 'lol.pbu:z4j:0.1.1'
    | **Maven** @@ -26,7 +26,7 @@ To get a local copy up and running, please follow the detailed setup instruction lol.pbu z4j - 0.1.0 + 0.1.1 ``` diff --git a/gradle.properties b/gradle.properties index 1c4bd25..7e7d882 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ micronautVersion=4.10.10 -z4jVersion=0.1.1-SNAPSHOT +z4jVersion=0.1.1 lombokVersion=1.18.44 dataFakerVersion=2.5.4 org.gradle.jvmargs=-Xmx4096M