diff --git a/src/test/java/com/slotline/SlotlineApplicationTests.java b/src/test/java/com/slotline/SlotlineApplicationTests.java new file mode 100644 index 0000000..58af295 --- /dev/null +++ b/src/test/java/com/slotline/SlotlineApplicationTests.java @@ -0,0 +1,12 @@ +package com.slotline; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SlotlineApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/src/test/java/com/slotline/recurrence/RecurrenceExpanderTest.java b/src/test/java/com/slotline/recurrence/RecurrenceExpanderTest.java new file mode 100644 index 0000000..ed54159 --- /dev/null +++ b/src/test/java/com/slotline/recurrence/RecurrenceExpanderTest.java @@ -0,0 +1,66 @@ +package com.slotline.recurrence; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RecurrenceExpanderTest { + + private final RecurrenceExpander expander = new RecurrenceExpander(); + + @Test + void expandsDailyWithIntervalAndCount() { + Instant start = Instant.parse("2026-09-01T10:00:00Z"); + Instant end = Instant.parse("2026-09-01T11:00:00Z"); + + List occurrences = expander.expand(start, end, "FREQ=DAILY;INTERVAL=2;COUNT=3", ZoneOffset.UTC); + + assertThat(occurrences).extracting(Occurrence::start).containsExactly( + Instant.parse("2026-09-01T10:00:00Z"), + Instant.parse("2026-09-03T10:00:00Z"), + Instant.parse("2026-09-05T10:00:00Z")); + assertThat(occurrences).allSatisfy(occurrence -> + assertThat(occurrence.end()).isEqualTo(occurrence.start().plusSeconds(3600))); + } + + @Test + void expandsWeeklyByDayInChronologicalOrder() { + Instant start = Instant.parse("2026-09-07T10:00:00Z"); + Instant end = Instant.parse("2026-09-07T11:00:00Z"); + + List occurrences = expander.expand(start, end, "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=4", ZoneOffset.UTC); + + assertThat(occurrences).extracting(Occurrence::start).containsExactly( + Instant.parse("2026-09-07T10:00:00Z"), + Instant.parse("2026-09-09T10:00:00Z"), + Instant.parse("2026-09-14T10:00:00Z"), + Instant.parse("2026-09-16T10:00:00Z")); + } + + @Test + void stopsAtUntilBoundary() { + Instant start = Instant.parse("2026-09-01T10:00:00Z"); + Instant end = Instant.parse("2026-09-01T11:00:00Z"); + + List occurrences = expander.expand(start, end, "FREQ=DAILY;UNTIL=20260903T100000Z", ZoneOffset.UTC); + + assertThat(occurrences).extracting(Occurrence::start).containsExactly( + Instant.parse("2026-09-01T10:00:00Z"), + Instant.parse("2026-09-02T10:00:00Z"), + Instant.parse("2026-09-03T10:00:00Z")); + } + + @Test + void rejectsUnboundedRule() { + Instant start = Instant.parse("2026-09-01T10:00:00Z"); + Instant end = Instant.parse("2026-09-01T11:00:00Z"); + + assertThatThrownBy(() -> expander.expand(start, end, "FREQ=DAILY", ZoneOffset.UTC)) + .isInstanceOf(RecurrenceException.class); + } +} diff --git a/src/test/java/com/slotline/security/JwtServiceTest.java b/src/test/java/com/slotline/security/JwtServiceTest.java new file mode 100644 index 0000000..c6f3d43 --- /dev/null +++ b/src/test/java/com/slotline/security/JwtServiceTest.java @@ -0,0 +1,45 @@ +package com.slotline.security; + +import com.slotline.config.SlotlineProperties; +import com.slotline.user.Role; +import io.jsonwebtoken.JwtException; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JwtServiceTest { + + private static JwtService serviceWithSecret(String secret) { + SlotlineProperties properties = new SlotlineProperties(); + properties.getJwt().setSecret(secret); + properties.getJwt().setIssuer("slotline"); + properties.getJwt().setExpiration(Duration.ofHours(1)); + return new JwtService(properties); + } + + @Test + void issuesAndParsesRoundTrip() { + JwtService jwtService = serviceWithSecret("round-trip-secret-round-trip-secret-0123456789"); + + String token = jwtService.issue(42L, "alice", Role.USER, 7L); + AuthenticatedUser parsed = jwtService.parse(token); + + assertThat(parsed.id()).isEqualTo(42L); + assertThat(parsed.username()).isEqualTo("alice"); + assertThat(parsed.role()).isEqualTo(Role.USER); + assertThat(parsed.organizationId()).isEqualTo(7L); + } + + @Test + void rejectsTokenSignedWithAnotherSecret() { + JwtService issuer = serviceWithSecret("issuer-secret-issuer-secret-issuer-0123456789"); + JwtService verifier = serviceWithSecret("verifier-secret-verifier-secret-diff-0123456789"); + + String token = issuer.issue(1L, "bob", Role.ADMIN, 3L); + + assertThatThrownBy(() -> verifier.parse(token)).isInstanceOf(JwtException.class); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..b3058df --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:h2:mem:slotline_concurrency;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000 + hikari: + maximum-pool-size: 20