From 4e0b4d4b1dfc34e0e3679039a4ce9b6650e166cb Mon Sep 17 00:00:00 2001
From: Jonathan Zollinger
Date: Tue, 14 Apr 2026 21:47:58 -0600
Subject: [PATCH 01/15] refactor: remove commented out properties
---
src/main/resources/z4j.yaml | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index ec029e8..2d5fbda 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -1398,12 +1398,10 @@ 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
@@ -1413,7 +1411,6 @@ components:
outdated:
type: boolean
description: Whether the category is out of date
- # readOnly: true
position:
type: integer
format: int64
@@ -1421,16 +1418,13 @@ components:
source_locale:
type: string
description: The source (default) locale of the category
- # readOnly: true
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:
From d7b0c5b8c0f6edfc1bdd7b2a38615b577c32c5fb Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Thu, 9 Apr 2026 21:39:55 -0600
Subject: [PATCH 02/15] refactor: correct html syntax for hrefs
---
src/main/resources/z4j.yaml | 164 ++++++++++++++++++------------------
1 file changed, 82 insertions(+), 82 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 2d5fbda..996f293 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
+
- Allowed For
+ 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
@@ -709,7 +709,7 @@ paths:
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
+ 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
@@ -808,7 +808,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 +852,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 +902,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 +980,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 +994,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 +1148,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
@@ -1449,13 +1449,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
@@ -1566,7 +1566,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
@@ -1885,13 +1885,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
@@ -1964,7 +1964,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
@@ -1983,11 +1983,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
@@ -2253,7 +2253,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
@@ -2262,12 +2262,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
@@ -2276,7 +2276,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:
@@ -2290,39 +2290,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
@@ -2338,7 +2338,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
@@ -2375,12 +2375,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
@@ -2396,13 +2396,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
@@ -2410,7 +2410,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
@@ -2429,7 +2429,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
@@ -2440,14 +2440,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:
@@ -2465,7 +2465,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
@@ -2477,7 +2477,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
@@ -2491,15 +2491,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
@@ -2573,18 +2573,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
@@ -2597,7 +2597,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
@@ -2614,7 +2614,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:
@@ -2694,13 +2694,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
@@ -2742,7 +2742,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
@@ -2782,7 +2782,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:
@@ -2806,20 +2806,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:
@@ -2827,7 +2827,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:
@@ -2835,7 +2835,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.
@@ -2864,7 +2864,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
@@ -2911,7 +2911,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
@@ -2927,20 +2927,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
@@ -2948,30 +2948,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:
@@ -3351,7 +3351,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
From 060bf21efb2f273c6eda0daed8241260b22fc833 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Tue, 14 Apr 2026 13:30:10 -0600
Subject: [PATCH 03/15] test: wip for no locale category queries
---
src/main/resources/z4j.yaml | 32 ++++++++++++++++++-
.../pbu/z4j/client/CategoryClientSpec.groovy | 12 +++++++
2 files changed, 43 insertions(+), 1 deletion(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 996f293..1a37b60 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -623,6 +623,14 @@ paths:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ category:
+ $ref: '#/components/schemas/Category'
responses:
"200":
description: OK Response
@@ -715,6 +723,14 @@ paths:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ category:
+ $ref: '#/components/schemas/Category'
responses:
"201":
description: OK Response
@@ -761,6 +777,14 @@ paths:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ category:
+ $ref: '#/components/schemas/Category'
responses:
"200":
description: OK Response
@@ -1418,6 +1442,12 @@ components:
source_locale:
type: string
description: The source (default) locale of the category
+ # readOnly: true
+ translations:
+ type: array
+ description: The translations for the category
+ items:
+ $ref: '#/components/schemas/Translation'
updated_at:
type: string
format: date-time
@@ -2967,7 +2997,7 @@ components:
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
diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
index 3a4d33c..6ec3b50 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -80,6 +80,18 @@ class CategoryClientSpec extends Z4jSpec {
where:
[[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations()
}
+
+ def "can use CreateCategoryNoLocale as an #userType"(CategoryClient categoryClient, String userType){
+ given:
+ CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest()
+ String categoryName = faker.animal().name()
+
+ when:
+ CategoryResponse response = categoryClient.createCategoryNoLocale(createCategoryRequest).block()
+
+ }
+
+
def "cannot use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) {
given:
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest()
From d978d35f5e00ac243457d9bfb0debbb31f9c0ef6 Mon Sep 17 00:00:00 2001
From: Jonathan Zollinger
Date: Tue, 14 Apr 2026 22:54:57 -0600
Subject: [PATCH 04/15] refactor: wip - add locale enum
---
src/main/resources/z4j.yaml | 80 ++++++++++++++++++-
.../pbu/z4j/client/CategoryClientSpec.groovy | 21 ++++-
2 files changed, 95 insertions(+), 6 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 1a37b60..179cfc9 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -1888,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:
@@ -3154,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
diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
index 6ec3b50..6aad648 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -81,14 +81,25 @@ class CategoryClientSpec extends Z4jSpec {
[[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations()
}
- def "can use CreateCategoryNoLocale as an #userType"(CategoryClient categoryClient, String userType){
+ def "can use CreateCategoryNoLocale as an #userType"(CategoryClient categoryClient, String userType) {
given:
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest()
String categoryName = faker.animal().name()
+ Category category = new Category(categoryName)
+ .setTranslations(List.of(new Translation().setLocale( "fr").set, faker.backToTheFuture().quote)))
+ createCategoryRequest.setCategory(category)
+
when:
CategoryResponse response = categoryClient.createCategoryNoLocale(createCategoryRequest).block()
+ then:
+ noExceptionThrown()
+
+ where:
+ [[categoryClient, userType]] << [[[adminCategoryClient, "admin"]]].combinations()
+
+
}
@@ -112,7 +123,8 @@ 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()
@@ -140,7 +152,7 @@ class CategoryClientSpec extends Z4jSpec {
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()
+ String categoryName = faker.bluey().quote() + " " + UUID.randomUUID().toString()
Category category = new Category(categoryName)
category.setDescription(faker.lordOfTheRings().location())
createCategoryRequest.setCategory(category)
@@ -158,7 +170,8 @@ class CategoryClientSpec extends Z4jSpec {
cleanup:
try {
adminCategoryClient.deleteCategory(locale, response.getCategory().getId())
- }catch (NullPointerException ignored){}
+ } catch (NullPointerException ignored) {
+ }
where:
[[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations()
From e2c55374ead48e31167072eabfbfdbe377415829 Mon Sep 17 00:00:00 2001
From: Jonathan Zollinger
Date: Wed, 15 Apr 2026 17:29:56 -0500
Subject: [PATCH 05/15] test: validate multiple translations can be provided
when creating a category (wip)
---
CONTRIBUTING.md | 14 ++++++++++++++
src/main/resources/z4j.yaml | 6 ------
.../lol/pbu/z4j/client/CategoryClientSpec.groovy | 3 ++-
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f1b7396..d957101 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,6 +45,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
@@ -217,6 +219,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 +304,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
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 179cfc9..1ddba7c 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -3261,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
diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
index 6aad648..689389d 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -85,8 +85,9 @@ class CategoryClientSpec extends Z4jSpec {
given:
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest()
String categoryName = faker.animal().name()
+ def translations = List.of(new Translation(LocaleAbbv.FR, faker.backToTheFuture().quote()))
Category category = new Category(categoryName)
- .setTranslations(List.of(new Translation().setLocale( "fr").set, faker.backToTheFuture().quote)))
+ .setTranslations(List.of(new Translation().setLocale( "fr").set, faker.backToTheFuture().quote))
createCategoryRequest.setCategory(category)
From 766c3176047ebcef1a6d53bdd0ce4c4b1a1c838c Mon Sep 17 00:00:00 2001
From: Jonathan Zollinger
Date: Wed, 15 Apr 2026 17:30:24 -0500
Subject: [PATCH 06/15] docs: cleanup and reword contributing doc
---
CONTRIBUTING.md | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d957101..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
@@ -60,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.
+
+
@@ -129,6 +132,7 @@ A dropdown menu to categorize the ticket's subject.
* **Field ID**: `40971535122835`
**Field values:**
+
| Field option title | Tag |
| :----------------- | :---- |
| Delivery | `delivery` |
@@ -144,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` | |
@@ -313,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
From 268d477f596e23b74247a21bbf91740925535d94 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 15:58:44 -0600
Subject: [PATCH 07/15] build: remove unused jacoco configuration
---
build.gradle.kts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 0e1dab2..77d2d49 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -87,11 +87,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 {
From 053b8bfeb01567531957af9e352e5d24b973a32d Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 16:23:28 -0600
Subject: [PATCH 08/15] test: add logback setup for testing
---
build.gradle.kts | 1 +
src/test/resources/Application-test.yaml | 3 +++
src/test/resources/logback.xml | 13 +++++++++++++
3 files changed, 17 insertions(+)
create mode 100644 src/test/resources/Application-test.yaml
create mode 100644 src/test/resources/logback.xml
diff --git a/build.gradle.kts b/build.gradle.kts
index 77d2d49..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 {
diff --git a/src/test/resources/Application-test.yaml b/src/test/resources/Application-test.yaml
new file mode 100644
index 0000000..bf5ab53
--- /dev/null
+++ b/src/test/resources/Application-test.yaml
@@ -0,0 +1,3 @@
+logger:
+ levels:
+ io.micronaut.http.client: WARN
\ 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 e77991e867890eede192c0bd4ad0c51b73d2e159 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 16:29:14 -0600
Subject: [PATCH 09/15] refactor(test): add primer Application-test.yaml
---
src/test/resources/Application-test.yaml | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/test/resources/Application-test.yaml b/src/test/resources/Application-test.yaml
index bf5ab53..2946078 100644
--- a/src/test/resources/Application-test.yaml
+++ b/src/test/resources/Application-test.yaml
@@ -1,3 +1,8 @@
logger:
levels:
- io.micronaut.http.client: WARN
\ No newline at end of file
+ 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
From 1144f2ad79daaf33859b76dfc3ce525dc5a2a394 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 20:14:28 -0600
Subject: [PATCH 10/15] refactor: use locale enum
---
src/main/resources/z4j.yaml | 11 ++-----
src/test/groovy/lol/pbu/z4j/Z4jSpec.groovy | 1 +
.../pbu/z4j/client/ArticleClientSpec.groovy | 7 +++--
.../pbu/z4j/client/CategoryClientSpec.groovy | 31 ++++++++++++-------
.../pbu/z4j/client/SearchClientSpec.groovy | 1 +
.../pbu/z4j/client/TicketClientSpec.groovy | 5 ---
6 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 1ddba7c..6ebede7 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -1427,8 +1427,7 @@ components:
format: int64
description: Automatically assigned when creating categories
locale:
- type: string
- description: The locale where the category is displayed
+ $ref: '#/components/schemas/LocaleAbbreviation'
name:
type: string
description: The name of the category
@@ -1440,9 +1439,7 @@ components:
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
@@ -3678,9 +3675,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 689389d..f4b9dd7 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -1,6 +1,5 @@
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
@@ -10,6 +9,7 @@ import spock.lang.Shared
import static io.micronaut.http.HttpStatus.FORBIDDEN
@MicronautTest
+@SuppressWarnings("GroovyAssignabilityCheck")
class CategoryClientSpec extends Z4jSpec {
@Shared
@@ -19,19 +19,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 +60,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()
@@ -81,22 +81,31 @@ class CategoryClientSpec extends Z4jSpec {
[[categoryClient, userType], locale] << [[[adminCategoryClient, "admin"]], allLocales].combinations()
}
- def "can use CreateCategoryNoLocale as an #userType"(CategoryClient categoryClient, String userType) {
+ 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(LocaleAbbv.FR, faker.backToTheFuture().quote()))
+ 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)
- .setTranslations(List.of(new Translation().setLocale( "fr").set, faker.backToTheFuture().quote))
+ .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()
@@ -104,7 +113,7 @@ class CategoryClientSpec extends Z4jSpec {
}
- def "cannot use CreateCategory as an #userType for the '#locale' locale"(CategoryClient categoryClient, String userType, String locale) {
+ 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()
@@ -131,7 +140,7 @@ class CategoryClientSpec extends Z4jSpec {
[[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 "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()
@@ -150,7 +159,7 @@ 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()
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
From 3fe704640d6af452a23996bde290b64274267879 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 20:14:48 -0600
Subject: [PATCH 11/15] docs: add / update cleanup scripts
---
cleanup_categories.ps1 | 77 +++++++++++++++++++++++++++++++++++++++
cleanup_user_segments.ps1 | 18 ++++-----
2 files changed, 86 insertions(+), 9 deletions(-)
create mode 100644 cleanup_categories.ps1
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)"
From 74426974029d8e5e9e0ebfb5774537e6e7ab24fa Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 20:40:26 -0600
Subject: [PATCH 12/15] fix: correct locale enum usage
---
.../z4j/model/TranslationsResponseSpec.groovy | 22 ++++++++++++++-----
1 file changed, 16 insertions(+), 6 deletions(-)
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()
}
}
From c27e031c6ed8cc8d93167123985d6ddcae6713f0 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 20:55:42 -0600
Subject: [PATCH 13/15] test: add tests for ShowCategory
---
src/main/resources/z4j.yaml | 3 +
.../pbu/z4j/client/CategoryClientSpec.groovy | 76 ++++++++++++++++++-
2 files changed, 75 insertions(+), 4 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 6ebede7..6a55cc1 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -754,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
diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
index f4b9dd7..1220f44 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -6,6 +6,7 @@ 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
@@ -178,12 +179,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()
+ }
}
From 4943138e4d37275445faa4a1201981d2d75e89fb Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 20:57:51 -0600
Subject: [PATCH 14/15] test: add negative tests for createCategoryNoLocale
---
.../pbu/z4j/client/CategoryClientSpec.groovy | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
index 1220f44..11052ad 100644
--- a/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
+++ b/src/test/groovy/lol/pbu/z4j/client/CategoryClientSpec.groovy
@@ -141,6 +141,27 @@ class CategoryClientSpec extends Z4jSpec {
[[categoryClient, userType], locale] << [[[userCategoryClient, "user"], [agentCategoryClient, "agent"]], allLocales].combinations()
}
+ 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()
From 001bc14a3f02c2c2b61bfa4d37493423ae4bb858 Mon Sep 17 00:00:00 2001
From: jonathan zollinger
Date: Mon, 20 Apr 2026 21:24:09 -0600
Subject: [PATCH 15/15] docs: correct usage of createCategoryNoLocale
---
src/main/resources/z4j.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml
index 6a55cc1..7de98a7 100644
--- a/src/main/resources/z4j.yaml
+++ b/src/main/resources/z4j.yaml
@@ -716,8 +716,8 @@ 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