signal-cli/src/test/java/org/asamk/signal/util/TimeParserTest.java
SiteRelEnby 8534a14602
Add notification profile commands
New listNotificationProfiles command to show the notification profiles
of the account and the current manual override, and a new
setNotificationProfile command to manually turn a profile on for a
duration or until a point in time, turn it off, or clear the manual
override. The override is written to the account storage record and
synced to the other devices, the same way the official clients do it.

Durations accept compact forms (90m, 1h37m), words (2 hours 30 minutes),
h:mm and ISO-8601. End times accept times of day, dates, ISO timestamps,
unix timestamps and "forever".
2026-09-12 11:57:33 -04:00

171 lines
8.5 KiB
Java

package org.asamk.signal.util;
import org.junit.jupiter.api.Test;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TimeParserTest {
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
// Thursday 2026-09-03 14:30:00 in Berlin (CEST, UTC+2)
private static final ZonedDateTime NOW = ZonedDateTime.of(2026, 9, 3, 14, 30, 0, 0, ZONE);
private static final Clock CLOCK = Clock.fixed(NOW.toInstant(), ZONE);
private static Duration duration(String input) throws TimeParser.TimeParseException {
return TimeParser.parseDuration(input);
}
private static ZonedDateTime timestamp(String input) throws TimeParser.TimeParseException {
return Instant.ofEpochMilli(TimeParser.parseTimestamp(input, CLOCK)).atZone(ZONE);
}
@Test
void parsesCompactDurations() throws Exception {
assertEquals(Duration.ofMinutes(90), duration("90m"));
assertEquals(Duration.ofMinutes(97), duration("1h37m"));
assertEquals(Duration.ofMinutes(97), duration("1h 37m"));
assertEquals(Duration.ofHours(2), duration("2h"));
assertEquals(Duration.ofDays(1).plusHours(2).plusMinutes(30).plusSeconds(15), duration("1d 2h 30m 15s"));
assertEquals(Duration.ofDays(14), duration("2w"));
assertEquals(Duration.ofMillis(1500), duration("1500ms"));
}
@Test
void parsesWordyDurations() throws Exception {
assertEquals(Duration.ofHours(2), duration("2 hours"));
assertEquals(Duration.ofMinutes(150), duration("2 hours 30 minutes"));
assertEquals(Duration.ofMinutes(150), duration("2 hours, 30 minutes"));
assertEquals(Duration.ofMinutes(150), duration("2 hours and 30 minutes"));
assertEquals(Duration.ofHours(1), duration("an hour"));
assertEquals(Duration.ofMinutes(1), duration("a minute"));
assertEquals(Duration.ofHours(3), duration("for 3 hrs"));
assertEquals(Duration.ofMinutes(45), duration(" 45 MINUTES "));
}
@Test
void parsesFractionalDurations() throws Exception {
assertEquals(Duration.ofMinutes(90), duration("1.5h"));
assertEquals(Duration.ofMinutes(90), duration("1,5 hours"));
assertEquals(Duration.ofSeconds(30), duration("0.5m"));
}
@Test
void parsesColonAndIsoDurations() throws Exception {
assertEquals(Duration.ofMinutes(90), duration("1:30"));
assertEquals(Duration.ofHours(1).plusMinutes(30).plusSeconds(10), duration("1:30:10"));
assertEquals(Duration.ofMinutes(150), duration("PT2H30M"));
assertEquals(Duration.ofMinutes(150), duration("pt2h30m"));
assertEquals(Duration.ofDays(1).plusHours(12), duration("P1DT12H"));
}
@Test
void rejectsInvalidDurations() {
assertThrows(TimeParser.TimeParseException.class, () -> duration("90"));
assertThrows(TimeParser.TimeParseException.class, () -> duration(""));
assertThrows(TimeParser.TimeParseException.class, () -> duration("0m"));
assertThrows(TimeParser.TimeParseException.class, () -> duration("2 fortnights"));
assertThrows(TimeParser.TimeParseException.class, () -> duration("1:75"));
assertThrows(TimeParser.TimeParseException.class, () -> duration("18:00pm"));
assertThrows(TimeParser.TimeParseException.class, () -> duration("PT"));
assertThrows(TimeParser.TimeParseException.class, () -> duration("soon"));
}
@Test
void parsesUnixTimestamps() throws Exception {
assertEquals(1_780_000_000_000L, TimeParser.parseTimestamp("1780000000", CLOCK));
assertEquals(1_780_000_000_123L, TimeParser.parseTimestamp("1780000000123", CLOCK));
}
@Test
void parsesIsoTimestamps() throws Exception {
assertEquals(Instant.parse("2026-09-03T18:00:00Z").toEpochMilli(),
TimeParser.parseTimestamp("2026-09-03T18:00:00Z", CLOCK));
assertEquals(Instant.parse("2026-09-03T18:00:00.500Z").toEpochMilli(),
TimeParser.parseTimestamp("2026-09-03T18:00:00.500Z", CLOCK));
assertEquals(Instant.parse("2026-09-03T16:00:00Z").toEpochMilli(),
TimeParser.parseTimestamp("2026-09-03T18:00:00+02:00", CLOCK));
assertEquals(Instant.parse("2026-09-03T16:00:00Z").toEpochMilli(),
TimeParser.parseTimestamp("2026-09-03T18:00:00+02:00[Europe/Berlin]", CLOCK));
}
@Test
void parsesLocalDateTimes() throws Exception {
assertEquals(NOW.withHour(18).withMinute(0), timestamp("2026-09-03T18:00"));
assertEquals(NOW.withHour(18).withMinute(0), timestamp("2026-09-03 18:00"));
assertEquals(NOW.withHour(18).withMinute(0).withSecond(30), timestamp("2026-09-03 18:00:30"));
assertEquals(NOW.withHour(9).withMinute(5), timestamp("2026-09-03 9:05"));
assertEquals(NOW.plusDays(2).withHour(0).withMinute(0), timestamp("2026-09-05"));
}
@Test
void parsesTimesOfDayAsNextOccurrence() throws Exception {
// later today
assertEquals(NOW.withHour(18).withMinute(0), timestamp("18:00"));
assertEquals(NOW.withHour(18).withMinute(0), timestamp("6pm"));
assertEquals(NOW.withHour(18).withMinute(30), timestamp("6:30pm"));
assertEquals(NOW.withHour(18).withMinute(30), timestamp("6:30 PM"));
assertEquals(NOW.withHour(18).withMinute(30), timestamp("6:30 p.m."));
assertEquals(NOW.withHour(14).withMinute(45).withSecond(10), timestamp("14:45:10"));
assertEquals(NOW.withHour(18).withMinute(0), timestamp("until 18:00"));
assertEquals(NOW.withHour(18).withMinute(0), timestamp("at 18:00"));
// already passed today, so tomorrow
assertEquals(NOW.plusDays(1).withHour(9).withMinute(0), timestamp("9:00"));
assertEquals(NOW.plusDays(1).withHour(9).withMinute(0), timestamp("9am"));
assertEquals(NOW.plusDays(1).withHour(12).withMinute(0), timestamp("noon"));
assertEquals(NOW.plusDays(1).withHour(0).withMinute(0), timestamp("midnight"));
assertEquals(NOW.plusDays(1).withHour(0).withMinute(0), timestamp("12am"));
assertEquals(NOW.plusDays(1).withHour(12).withMinute(0), timestamp("12pm"));
// exactly now counts as passed
assertEquals(NOW.plusDays(1), timestamp("14:30"));
}
@Test
void parsesRelativeDays() throws Exception {
assertEquals(NOW.plusDays(1).withHour(0).withMinute(0), timestamp("tomorrow"));
assertEquals(NOW.plusDays(1).withHour(9).withMinute(0), timestamp("tomorrow 9:00"));
assertEquals(NOW.plusDays(1).withHour(9).withMinute(0), timestamp("tomorrow at 9am"));
assertEquals(NOW.plusDays(1).withHour(12).withMinute(0), timestamp("tomorrow noon"));
// "today" is explicit and doesn't roll over
assertEquals(NOW.withHour(9).withMinute(0), timestamp("today 9:00"));
}
@Test
void rejectsInvalidTimestamps() {
assertThrows(TimeParser.TimeParseException.class, () -> timestamp(""));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("6"));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("25:00"));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("13pm"));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("2026-13-01"));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("90m"));
assertThrows(TimeParser.TimeParseException.class, () -> timestamp("later"));
}
@Test
void detectsDurations() {
assertTrue(TimeParser.looksLikeDuration("90m"));
assertTrue(TimeParser.looksLikeDuration("2 hours"));
assertTrue(TimeParser.looksLikeDuration("2 hours and 30 minutes"));
assertFalse(TimeParser.looksLikeDuration("tomorrow"));
assertFalse(TimeParser.looksLikeDuration("6pm"));
assertFalse(TimeParser.looksLikeDuration("1780000000"));
}
@Test
void formatsDurations() {
assertEquals("2h 30m", TimeParser.formatDuration(Duration.ofMinutes(150)));
assertEquals("1d 2h 30m 15s", TimeParser.formatDuration(Duration.ofDays(1).plusHours(2).plusMinutes(30).plusSeconds(15)));
assertEquals("0s", TimeParser.formatDuration(Duration.ZERO));
assertEquals("45s", TimeParser.formatDuration(Duration.ofMillis(45_999)));
assertEquals("-5m", TimeParser.formatDuration(Duration.ofMinutes(-5)));
}
}