diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index 034789d..f3dfc4a 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -23,11 +23,16 @@ import java.io.File; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.text.SimpleDateFormat; import java.util.*; +import net.rcarz.utils.WorklogUtils; import net.sf.json.JSON; import net.sf.json.JSONArray; import net.sf.json.JSONObject; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; /** * Represents a JIRA issue. @@ -1077,6 +1082,36 @@ public class Issue extends Resource { return new Comment(restclient, (JSONObject) result, key); } + /** + * Adds {@link WorkLog} to this issue + * @param comment provided comment + * @param startDate provided start date + * @param timeSpentSeconds provided time spent. This cannot be lower than 1m inute + * @return + * @throws JiraException when worklog creation fails + */ + public WorkLog addWorkLog(String comment, DateTime startDate, long timeSpentSeconds) throws JiraException { + try { + if (comment == null) + throw new IllegalArgumentException("Invalid comment."); + if (startDate == null) + throw new IllegalArgumentException("Invalid start time."); + if (timeSpentSeconds < 60) // We do not add a worklog that duration is below a minute + throw new IllegalArgumentException("Time spent cannot be lower than 1 minute."); + + JSONObject req = new JSONObject(); + req.put("comment", comment); + req.put("started", DateTimeFormat.forPattern(Field.DATETIME_FORMAT).print(startDate.getMillis())); + req.put("timeSpent", WorklogUtils.formatDurationFromSeconds(timeSpentSeconds)); + + JSON result = restclient.post(getRestUri(key) + "/worklog", req); + JSONObject jo = (JSONObject) result; + return new WorkLog(restclient, jo); + } catch (Exception ex) { + throw new JiraException("Failed add worklog to issue " + key, ex); + } + } + /** * Links this issue with another issue. * diff --git a/src/main/java/net/rcarz/utils/WorklogUtils.java b/src/main/java/net/rcarz/utils/WorklogUtils.java new file mode 100644 index 0000000..d50d29a --- /dev/null +++ b/src/main/java/net/rcarz/utils/WorklogUtils.java @@ -0,0 +1,40 @@ +package net.rcarz.utils; + +import net.rcarz.jiraclient.WorkLog; +import org.joda.time.DurationFieldType; +import org.joda.time.Period; +import org.joda.time.PeriodType; + +/** + * Created by mariusmerkevicius on 1/30/16. + * A set of utils static methods help set {@link WorkLog} + */ +public class WorklogUtils { + + /** + * Formats duration time into pretty string format + * Does not output seconds + * + * @param durationInSeconds provided duration to format + * @return formatted duration + */ + public static String formatDurationFromSeconds(long durationInSeconds) { + if (durationInSeconds < 60) + return "0m"; + StringBuilder builder = new StringBuilder(); + PeriodType type = PeriodType.forFields(new DurationFieldType[]{ + DurationFieldType.hours(), + DurationFieldType.minutes() + }); + + Period period = new Period(1000 * durationInSeconds, type); + if (period.getHours() != 0) + builder.append(period.getHours()).append("h").append(" "); + if (period.getMinutes() != 0) + builder.append(period.getMinutes()).append("m").append(" "); + if ((builder.length() > 0) && builder.charAt(builder.length() - 1) == " ".charAt(0)) + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + +} diff --git a/src/test/java/net/rcarz/jiraclient/IssueWorklogTest.java b/src/test/java/net/rcarz/jiraclient/IssueWorklogTest.java new file mode 100644 index 0000000..f6fd2d5 --- /dev/null +++ b/src/test/java/net/rcarz/jiraclient/IssueWorklogTest.java @@ -0,0 +1,109 @@ +package net.rcarz.jiraclient; + +import net.sf.json.JSON; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; +import org.joda.time.DateTime; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.*; + +/** + * Created by mariusmerkevicius on 1/30/16. + */ +public class IssueWorklogTest { + + @Test + public void testParsing_inputValid_shouldCreateJsonObject() throws Exception { + // Arrange + // Act + JSONObject worklogObject = (JSONObject) JSONSerializer.toJSON(RESPONSE_WORKLOG_BODY); + + // Assert + assertNotNull(worklogObject); + } + + @Test + public void testParsing_inputValidJson_shouldCreateWorklog() throws Exception { + // Arrange + // Act + WorkLog workLog = new WorkLog(mock(RestClient.class), (JSONObject) JSONSerializer.toJSON(RESPONSE_WORKLOG_BODY)); + + // Assert + assertNotNull(workLog); + assertNotNull(workLog.getAuthor()); + assertEquals("https://jira.test.lt/rest/api/2/issue/32374/worklog/80720", workLog.getSelf()); + assertEquals("80720", workLog.getId()); + assertEquals("Test", workLog.getComment()); + assertEquals(1454179576583L, workLog.getCreatedDate().getTime()); + assertEquals(1454179576583L, workLog.getUpdatedDate().getTime()); + assertEquals(1453879853201L, workLog.getStarted().getTime()); + assertEquals("5m", workLog.getTimeSpent()); + assertEquals(300, workLog.getTimeSpentSeconds()); + } + + @Test + public void testAdding_inputValid_shouldInvokeAdding() throws Exception { + // Arrange + Issue issue = mock(Issue.class); + issue.restclient = mock(RestClient.class); + doCallRealMethod().when(issue).addWorkLog(anyString(), any(DateTime.class), anyLong()); + + // Act + issue.addWorkLog("test", DateTime.now(), 60); + + // Assert + verify(issue.restclient).post(anyString(), any(JSON.class)); + } + + @Test(expected = JiraException.class) + public void testAdding_inputNullComment_shouldNotAdd() throws Exception { + // Arrange + Issue issue = mock(Issue.class); + issue.restclient = mock(RestClient.class); + doCallRealMethod().when(issue).addWorkLog(anyString(), any(DateTime.class), anyLong()); + + // Act + // Assert + issue.addWorkLog(null, DateTime.now(), 120); + } + + @Test(expected = JiraException.class) + public void testAdding_inputNullDateTime_shouldNotAdd() throws Exception { + // Arrange + Issue issue = mock(Issue.class); + issue.restclient = mock(RestClient.class); + doCallRealMethod().when(issue).addWorkLog(anyString(), any(DateTime.class), anyLong()); + + // Act + // Assert + issue.addWorkLog("asdf", null, 120); + } + + @Test(expected = JiraException.class) + public void testAdding_inputDurationTooLow_shouldNotAdd() throws Exception { + // Arrange + Issue issue = mock(Issue.class); + issue.restclient = mock(RestClient.class); + doCallRealMethod().when(issue).addWorkLog(anyString(), any(DateTime.class), anyLong()); + + // Act + // Assert + issue.addWorkLog("asdf", DateTime.now(), 30); + } + + + //region Constants + + // Mock response from jira + + public static final String RESPONSE_WORKLOG_BODY = "{\"self\":\"https://jira.test.lt/rest/api/2/issue/32374/worklog/80720\",\"author\":{\"self\":\"https://jira.test.lt/rest/api/2/user?username=test%40test.lt\",\"name\":\"test@test.lt\",\"key\":\"test@test.lt\",\"emailAddress\":\"test@test.lt\",\"avatarUrls\":{\"48x48\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=48\",\"24x24\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=24\",\"16x16\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=16\",\"32x32\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=32\"},\"displayName\":\"Marius Merkevicius\",\"active\":true,\"timeZone\":\"Europe/Vilnius\"},\"updateAuthor\":{\"self\":\"https://jira.test.lt/rest/api/2/user?username=test%40test.lt\",\"name\":\"test@test.lt\",\"key\":\"test@test.lt\",\"emailAddress\":\"test@test.lt\",\"avatarUrls\":{\"48x48\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=48\",\"24x24\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=24\",\"16x16\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=16\",\"32x32\":\"https://secure.gravatar.com/avatar/e4dacfe8f27cb89341bf990e556a4be0?d=mm&s=32\"},\"displayName\":\"Marius Merkevicius\",\"active\":true,\"timeZone\":\"Europe/Vilnius\"},\"comment\":\"Test\",\"created\":\"2016-01-30T20:46:16.583+0200\",\"updated\":\"2016-01-30T20:46:16.583+0200\",\"started\":\"2016-01-27T09:30:53.201+0200\",\"timeSpent\":\"5m\",\"timeSpentSeconds\":300,\"id\":\"80720\"}"; + + //endregion + +} \ No newline at end of file diff --git a/src/test/java/net/rcarz/jiraclient/WorklogTest.java b/src/test/java/net/rcarz/jiraclient/WorklogTest.java index 2d6e12f..43b513b 100644 --- a/src/test/java/net/rcarz/jiraclient/WorklogTest.java +++ b/src/test/java/net/rcarz/jiraclient/WorklogTest.java @@ -10,6 +10,7 @@ import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Locale; import static junit.framework.Assert.assertEquals; import static org.mockito.Matchers.anyString; @@ -17,7 +18,7 @@ import static org.mockito.Matchers.anyString; @RunWith(PowerMockRunner.class) public class WorklogTest { - private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + private SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Field.DATETIME_FORMAT); @Test(expected = JiraException.class) public void testJiraExceptionFromRestException() throws Exception { @@ -48,7 +49,7 @@ public class WorklogTest { } @Test - public void testWorklog() { + public void testWorklog() throws Exception { List workLogs = Field.getResourceArray(WorkLog.class, Utils.getTestIssueWorklogs().get("worklogs"), null); assertEquals(2, workLogs.size()); @@ -59,11 +60,10 @@ public class WorklogTest { assertEquals("45517", workLog.getId()); String author = "joseph"; assertEquals(author, workLog.getAuthor().getName()); - final long expectedStartedUnixTimeStamp = 1439803140000L; //unix timestamp in millis of 2015-08-17T13:19:00.000+0400 - assertEquals(expectedStartedUnixTimeStamp, workLog.getStarted().getTime()); - final long expectedCreatedAndUpdatedUnitTimeStamp = 1440062384000L; //unix timestamp in millis of 2015-08-20T13:19:44.000+0400 - assertEquals(expectedCreatedAndUpdatedUnitTimeStamp, workLog.getCreatedDate().getTime()); - assertEquals(expectedCreatedAndUpdatedUnitTimeStamp, workLog.getUpdatedDate().getTime()); + String started = "2015-08-17T13:19:00.000+0400"; + assertEquals(simpleDateFormat.parse(started), workLog.getStarted()); + String created = "2015-08-20T13:19:44.000+0400"; + assertEquals(simpleDateFormat.parse(created), workLog.getCreatedDate()); assertEquals(21600, workLog.getTimeSpentSeconds()); assertEquals(author, workLog.getUpdateAuthor().getName()); } diff --git a/src/test/java/net/rcarz/utils/WorklogUtilsFormatDurationTest.java b/src/test/java/net/rcarz/utils/WorklogUtilsFormatDurationTest.java new file mode 100644 index 0000000..3447457 --- /dev/null +++ b/src/test/java/net/rcarz/utils/WorklogUtilsFormatDurationTest.java @@ -0,0 +1,80 @@ +package net.rcarz.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by mariusmerkevicius on 1/30/16. + */ +public class WorklogUtilsFormatDurationTest { + + @Test + public void testEmpty() throws Exception { + assertEquals("0m", WorklogUtils.formatDurationFromSeconds(0)); + } + + @Test + public void testNegative() throws Exception { + assertEquals("0m", WorklogUtils.formatDurationFromSeconds(-200)); + } + + @Test + public void testLowSecond() throws Exception { + assertEquals("0m", WorklogUtils.formatDurationFromSeconds(1)); + } + + @Test + public void testSeconds() throws Exception { + assertEquals("0m", WorklogUtils.formatDurationFromSeconds(59)); + } + + @Test + public void testMinutes() throws Exception { + assertEquals("1m", WorklogUtils.formatDurationFromSeconds(60)); + } + + @Test + public void testMinutesAndSeconds() throws Exception { + assertEquals("1m", WorklogUtils.formatDurationFromSeconds( + 60 // 1 minute + + 2) // 2 seconds + ); + } + + @Test + public void testMinutesAndSeconds2() throws Exception { + assertEquals("2m", WorklogUtils.formatDurationFromSeconds( + 60 // 1 minute + + 72) // 72 seconds + ); + } + + @Test + public void testHours() throws Exception { + assertEquals("1h 10m", WorklogUtils.formatDurationFromSeconds( + (60 * 60) // 1 hour + + (10 * 60) // 10 minutes + + 3) // 3 seconds + ); + } + + @Test + public void testDays() throws Exception { + assertEquals("50h 20m", WorklogUtils.formatDurationFromSeconds( + (60 * 60 * 50) // 50 hours + + (60 * 20) // 20 minutes + + (3) // s seconds + )); + } + + @Test + public void testDays2() throws Exception { + assertEquals("50h 22m", WorklogUtils.formatDurationFromSeconds( + (60 * 60 * 50) // 50 hours + + (60 * 20) // 20 minutes + + (125) // 125 seconds + )); + } + +} \ No newline at end of file