diff --git a/src/main/java/com/crowdin/client/tasks/TasksApi.java b/src/main/java/com/crowdin/client/tasks/TasksApi.java index 000ee36fe..8fda1c912 100644 --- a/src/main/java/com/crowdin/client/tasks/TasksApi.java +++ b/src/main/java/com/crowdin/client/tasks/TasksApi.java @@ -12,15 +12,7 @@ import com.crowdin.client.core.model.PatchRequest; import com.crowdin.client.core.model.ResponseList; import com.crowdin.client.core.model.ResponseObject; -import com.crowdin.client.tasks.model.AddTaskRequest; -import com.crowdin.client.tasks.model.AddTaskSettingsTemplateRequest; -import com.crowdin.client.tasks.model.Status; -import com.crowdin.client.tasks.model.Task; -import com.crowdin.client.tasks.model.TaskResponseList; -import com.crowdin.client.tasks.model.TaskResponseObject; -import com.crowdin.client.tasks.model.TaskSettingsTemplate; -import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseList; -import com.crowdin.client.tasks.model.TaskSettingsTemplateResponseObject; +import com.crowdin.client.tasks.model.*; import java.util.List; import java.util.Map; @@ -248,6 +240,85 @@ public ResponseObject editTaskSettingsTemplate(Long projec return ResponseObject.of(responseObject.getData()); } + /** + * @param projectId project identifier + * @param taskId task identifier + * @param limit maximum number of items to retrieve (default 25) + * @param offset starting offset in the collection (default 0) + * @return list of task comments + * @see + */ + public ResponseList listTasksComments(Long projectId, Long taskId, Integer limit, Integer offset) throws HttpException, HttpBadRequestException { + Map> queryParams = HttpRequestConfig.buildUrlParams( + "limit", Optional.ofNullable(limit), + "offset", Optional.ofNullable(offset) + ); + TaskCommentResponseList taskCommentResponseList = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", new HttpRequestConfig(queryParams), TaskCommentResponseList.class); + return TaskCommentResponseList.to(taskCommentResponseList); + } + + /** + * @param projectId project identifier + * @param taskId task identifier + * @param request request object + * @return newly created task comment + * @see + */ + public ResponseObject addTaskComment(Long projectId, Long taskId, CreateTaskCommentRequest request) throws HttpException, HttpBadRequestException { + TaskCommentResponseObject taskCommentResponseObject = this.httpClient.post(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments", request, new HttpRequestConfig(), TaskCommentResponseObject.class); + return ResponseObject.of(taskCommentResponseObject.getData()); + } + + /** + * @param projectId project identifier + * @param taskId task identifier + * @param commentId comment identifier + * @return task comment + * @see + */ + public ResponseObject getTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException { + TaskCommentResponseObject taskCommentResponseObject = this.httpClient.get(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), TaskCommentResponseObject.class); + return ResponseObject.of(taskCommentResponseObject.getData()); + } + + /** + * @param projectId project identifier + * @param taskId task identifier + * @param commentId task identifier + * @see + */ + public void deleteTaskComment(Long projectId, Long taskId, Long commentId) throws HttpException, HttpBadRequestException { + this.httpClient.delete(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, new HttpRequestConfig(), Void.class); + } + + /** + * @param projectId project identifier + * @param taskId task identifier + * @param commentId task identifier + * @param request request object + * @return updated task comment + * @see + */ + public ResponseObject editTaskComment(Long projectId, Long taskId, Long commentId, List request) throws HttpException, HttpBadRequestException { + TaskCommentResponseObject taskCommentResponseObject = this.httpClient.patch(this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + commentId, request, new HttpRequestConfig(), TaskCommentResponseObject.class); + return ResponseObject.of(taskCommentResponseObject.getData()); + } + // private String formUrl_taskSettingsTemplates(Long projectId) { diff --git a/src/main/java/com/crowdin/client/tasks/model/CreateTaskCommentRequest.java b/src/main/java/com/crowdin/client/tasks/model/CreateTaskCommentRequest.java new file mode 100644 index 000000000..eecd3cbf2 --- /dev/null +++ b/src/main/java/com/crowdin/client/tasks/model/CreateTaskCommentRequest.java @@ -0,0 +1,9 @@ +package com.crowdin.client.tasks.model; + +import lombok.Data; + +@Data +public class CreateTaskCommentRequest { + private String text; + private Long timeSpent; +} diff --git a/src/main/java/com/crowdin/client/tasks/model/TaskComment.java b/src/main/java/com/crowdin/client/tasks/model/TaskComment.java new file mode 100644 index 000000000..7d6774b7d --- /dev/null +++ b/src/main/java/com/crowdin/client/tasks/model/TaskComment.java @@ -0,0 +1,16 @@ +package com.crowdin.client.tasks.model; + +import lombok.Data; + +import java.util.Date; + +@Data +public class TaskComment { + private Long id; + private Long userId; + private Long taskId; + private String text; + private Long timeSpent; + private Date createdAt; + private Date updatedAt; +} diff --git a/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseList.java b/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseList.java new file mode 100644 index 000000000..bb3b50480 --- /dev/null +++ b/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseList.java @@ -0,0 +1,26 @@ +package com.crowdin.client.tasks.model; + +import com.crowdin.client.core.model.Pagination; +import com.crowdin.client.core.model.ResponseList; +import com.crowdin.client.core.model.ResponseObject; +import lombok.Data; + +import java.util.List; +import java.util.stream.Collectors; + +@Data +public class TaskCommentResponseList { + + private List data; + private Pagination pagination; + + public static ResponseList to(TaskCommentResponseList taskCommentResponseList) { + return ResponseList.of( + taskCommentResponseList.getData().stream() + .map(TaskCommentResponseObject::getData) + .map(ResponseObject::of) + .collect(Collectors.toList()), + taskCommentResponseList.getPagination() + ); + } +} diff --git a/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseObject.java b/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseObject.java new file mode 100644 index 000000000..598021741 --- /dev/null +++ b/src/main/java/com/crowdin/client/tasks/model/TaskCommentResponseObject.java @@ -0,0 +1,9 @@ +package com.crowdin.client.tasks.model; + +import lombok.Data; + +@Data +public class TaskCommentResponseObject { + + private TaskComment data; +} diff --git a/src/test/java/com/crowdin/client/tasks/TaskCommentsApiTest.java b/src/test/java/com/crowdin/client/tasks/TaskCommentsApiTest.java new file mode 100644 index 000000000..45bd273a5 --- /dev/null +++ b/src/test/java/com/crowdin/client/tasks/TaskCommentsApiTest.java @@ -0,0 +1,183 @@ +package com.crowdin.client.tasks; + +import com.crowdin.client.core.model.PatchOperation; +import com.crowdin.client.core.model.PatchRequest; +import com.crowdin.client.core.model.ResponseList; +import com.crowdin.client.core.model.ResponseObject; +import com.crowdin.client.framework.RequestMock; +import com.crowdin.client.framework.TestClient; +import com.crowdin.client.tasks.model.*; +import lombok.SneakyThrows; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.TimeZone; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +public class TaskCommentsApiTest extends TestClient { + + private final Long projectId = 12L; + private final Long taskId = 203L; + private final Long taskCommentId = 1233L; + private final Long userId = 5L; + + private TimeZone originalTimeZone; + + private static final Instant EXPECTED_DATE = Instant.parse("2025-08-23T09:04:29Z"); + + @BeforeEach + void setUp() { + originalTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + } + + @AfterEach + void tearDown() { + TimeZone.setDefault(originalTimeZone); + } + + @Override + public List getMocks() { + return Arrays.asList( + // LIST + RequestMock.build( + formUrl_taskComments(projectId, taskId), + HttpGet.METHOD_NAME, + "api/tasks/comments/listTaskCommentsResponse.json", + new HashMap() {{ + put("limit", 20); + put("offset", 10); + }} + ), + + // ADD + RequestMock.build( + formUrl_taskComments(projectId, taskId), + HttpPost.METHOD_NAME, + "api/tasks/comments/addTaskCommentRequest.json", + "api/tasks/comments/taskCommentsResponse_single.json" + ), + + // GET + RequestMock.build( + formUrl_taskCommentId(projectId, taskId, taskCommentId), + HttpGet.METHOD_NAME, + "api/tasks/comments/taskCommentsResponse_single.json" + ), + + // DELETE + RequestMock.build( + formUrl_taskCommentId(projectId, taskId, taskCommentId), + HttpDelete.METHOD_NAME + ), + + // PATCH + RequestMock.build( + formUrl_taskCommentId(projectId, taskId, taskCommentId), + HttpPatch.METHOD_NAME, + "api/tasks/comments/editTaskCommentTextRequest.json", + "api/tasks/comments/taskCommentsResponse_single.json" + ) + ); + } + + @Test + @SneakyThrows + public void listTaskCommentsTest() { + ResponseList response = this.getTasksApi().listTasksComments(projectId, taskId, 20, 10); + + assertNotNull(response.getData().get(0).getData()); + assertEquals(1, response.getData().size()); + + TaskComment comment = response.getData().get(0).getData(); + + assertTaskComment(comment); + } + + @Test + @SneakyThrows + public void addTaskCommentTest() { + CreateTaskCommentRequest request = new CreateTaskCommentRequest(); + request.setText("translate task"); + request.setTimeSpent(3600L); + + ResponseObject response = this.getTasksApi().addTaskComment(projectId, taskId, request); + + assertNotNull(response.getData()); + + TaskComment comment = response.getData(); + + assertTaskComment(comment); + } + + @Test + @SneakyThrows + public void getTaskCommentTest() { + ResponseObject response = this.getTasksApi().getTaskComment(projectId, taskId, taskCommentId); + + assertNotNull(response.getData()); + + TaskComment comment = response.getData(); + + assertTaskComment(comment); + } + + @Test + @SneakyThrows + public void editTaskCommentTest() { + PatchRequest request = new PatchRequest(); + request.setOp(PatchOperation.REPLACE); + request.setValue("translate task"); + request.setPath("/text"); + ResponseObject response = this.getTasksApi().editTaskComment(projectId, taskId, taskCommentId, singletonList(request)); + + assertNotNull(response.getData()); + + TaskComment comment = response.getData(); + + assertTaskComment(comment); + } + + @Test + @SneakyThrows + public void deleteTaskCommentTest() { + assertDoesNotThrow(() -> + this.getTasksApi().deleteTaskComment(projectId, taskId, taskCommentId) + ); + } + + @SneakyThrows + private void assertTaskComment(TaskComment comment) { + assertEquals(taskCommentId, comment.getId()); + assertEquals(taskId, comment.getTaskId()); + assertEquals(userId, comment.getUserId()); + assertEquals("translate task", comment.getText()); + assertEquals(3600L, comment.getTimeSpent()); + + assertNotNull(comment.getCreatedAt()); + assertNotNull(comment.getUpdatedAt()); + assertEquals(EXPECTED_DATE, comment.getCreatedAt().toInstant()); + assertEquals(EXPECTED_DATE, comment.getUpdatedAt().toInstant()); + } + + // + private String formUrl_taskComments(Long projectId, Long taskId) { + return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments"; + } + + private String formUrl_taskCommentId(Long projectId, Long taskId, Long taskCommentId) { + return this.url + "/projects/" + projectId + "/tasks/" + taskId + "/comments/" + taskCommentId; + } + // +} diff --git a/src/test/resources/api/tasks/comments/addTaskCommentRequest.json b/src/test/resources/api/tasks/comments/addTaskCommentRequest.json new file mode 100644 index 000000000..308da676a --- /dev/null +++ b/src/test/resources/api/tasks/comments/addTaskCommentRequest.json @@ -0,0 +1,4 @@ +{ + "text": "translate task", + "timeSpent": 3600 +} \ No newline at end of file diff --git a/src/test/resources/api/tasks/comments/editTaskCommentTextRequest.json b/src/test/resources/api/tasks/comments/editTaskCommentTextRequest.json new file mode 100644 index 000000000..a28bc0983 --- /dev/null +++ b/src/test/resources/api/tasks/comments/editTaskCommentTextRequest.json @@ -0,0 +1,7 @@ +[ + { + "op": "replace", + "path": "/text", + "value": "translate task" + } +] \ No newline at end of file diff --git a/src/test/resources/api/tasks/comments/listTaskCommentsResponse.json b/src/test/resources/api/tasks/comments/listTaskCommentsResponse.json new file mode 100644 index 000000000..f3906446e --- /dev/null +++ b/src/test/resources/api/tasks/comments/listTaskCommentsResponse.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "data": { + "id": 1233, + "userId": 5, + "taskId": 203, + "text": "translate task", + "timeSpent": 3600, + "createdAt": "2025-08-23T09:04:29+00:00", + "updatedAt": "2025-08-23T09:04:29+00:00" + } + } + ], + "pagination": { + "offset": 10, + "limit": 20 + } +} \ No newline at end of file diff --git a/src/test/resources/api/tasks/comments/taskCommentsResponse_single.json b/src/test/resources/api/tasks/comments/taskCommentsResponse_single.json new file mode 100644 index 000000000..7a4f77951 --- /dev/null +++ b/src/test/resources/api/tasks/comments/taskCommentsResponse_single.json @@ -0,0 +1,11 @@ +{ + "data": { + "id": 1233, + "userId": 5, + "taskId": 203, + "text": "translate task", + "timeSpent": 3600, + "createdAt": "2025-08-23T09:04:29+00:00", + "updatedAt": "2025-08-23T09:04:29+00:00" + } +} \ No newline at end of file