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:
+
+
+
+ | value |
+ description |
+
+
+
+
+ position |
+ order set manually using the Arrange Content page. Default order |
+
+
+ created_at |
+ order by creation time |
+
+
+ updated_at |
+ order by update time |
+
+
+
+ schema:
+ type: string
+ enum:
+ - position
+ - created_at
+ - updated_at
+ - name: sort_order
+ in: query
+ description: |
+
+
+
+ | value |
+ description |
+
+
+
+
+ asc |
+ ascending order |
+
+
+ desc |
+ descending 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
+
+
+ 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:
-
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
+
+ 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:
+ 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
+
+ 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
+ Returns a number of ticket properties though not the ticket comments. To get the comments, use List Comments
+ This endpoint supports pagination as described in 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:
+ 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
+ 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
+ Translations are embedded within the category because they're
+ not shared between resources.
responses:
"200":
description: description
@@ -761,6 +780,14 @@ paths:
+ 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