Skip to content

Commit

Permalink
#265: adopt backend to operate with timezone aware timestamps using U…
Browse files Browse the repository at this point in the history
…TC to provide timezone aware timestamps in APIs to fix data times in user interface
  • Loading branch information
Florian Gessner committed Dec 28, 2023
1 parent fae0ace commit 6a556a5
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 39 deletions.
2 changes: 1 addition & 1 deletion rest-playground.http
Original file line number Diff line number Diff line change
@@ -1 +1 @@
GET localhost:8080/api/email
GET localhost:8080/api/emails
10 changes: 6 additions & 4 deletions src/main/java/de/gessnerfl/fakesmtp/model/Email.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package de.gessnerfl.fakesmtp.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;

import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.*;

import static java.util.Comparator.comparing;
Expand Down Expand Up @@ -33,7 +34,8 @@ public class Email {
@Column(name = "received_on", nullable = false)
@Basic(optional = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime receivedOn;
@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone="UTC")
private ZonedDateTime receivedOn;

@Lob
@Column(name = "raw_data", nullable = false)
Expand Down Expand Up @@ -85,11 +87,11 @@ public void setSubject(String subject) {
this.subject = subject;
}

public LocalDateTime getReceivedOn() {
public ZonedDateTime getReceivedOn() {
return receivedOn;
}

public void setReceivedOn(LocalDateTime receivedOn) {
public void setReceivedOn(ZonedDateTime receivedOn) {
this.receivedOn = receivedOn;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

import jakarta.persistence.criteria.Path;

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class ExpressionValueHelper {
private ExpressionValueHelper(){}

public static Object convertDateIfApplicable(Path<?> path, Object value) {
return path.getJavaType().isAssignableFrom(LocalDateTime.class) ? parseDate(value) : value;
return path.getJavaType().isAssignableFrom(ZonedDateTime.class) ? parseDate(value) : value;
}

private static Object parseDate(Object value) {
String dateString = value.toString();
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
return ZonedDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

@Service
public class TimestampProvider {

public LocalDateTime now(){
return LocalDateTime.now();
public ZonedDateTime now(){
return ZonedDateTime.now(ZoneId.of("UTC"));
}

}
1 change: 1 addition & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ spring:
jackson:
serialization:
write-dates-as-timestamps: false
time-zone: "UTC"

springdoc:
swagger-ui:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
import org.apache.commons.lang3.RandomStringUtils;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

class EmailControllerUtil {

private EmailControllerUtil() { }

public static Email prepareRandomEmail(int minusMinutes) {
var randomToken = RandomStringUtils.randomAlphanumeric(6);
var receivedOn = LocalDateTime.now().minusMinutes(minusMinutes);
var receivedOn = getUtcNow().minusMinutes(minusMinutes);

var content = new EmailContent();
content.setContentType(ContentType.PLAIN);
Expand All @@ -31,7 +32,7 @@ public static Email prepareRandomEmail(int minusMinutes) {

public static Email prepareEmail(String subject, String toAdress, int minusMinutes) {
var randomToken = RandomStringUtils.randomAlphanumeric(6);
var receivedOn = LocalDateTime.now().minusMinutes(minusMinutes);
var receivedOn = getUtcNow().minusMinutes(minusMinutes);

var content = new EmailContent();
content.setContentType(ContentType.PLAIN);
Expand All @@ -47,7 +48,7 @@ public static Email prepareEmail(String subject, String toAdress, int minusMinut

public static Email prepareEmail(String subject, String toAdress, int minusMinutes, String messageId) {
var randomToken = RandomStringUtils.randomAlphanumeric(6);
var receivedOn = LocalDateTime.now().minusMinutes(minusMinutes);
var receivedOn = getUtcNow().minusMinutes(minusMinutes);

var content = new EmailContent();
content.setContentType(ContentType.PLAIN);
Expand All @@ -61,12 +62,16 @@ public static Email prepareEmail(String subject, String toAdress, int minusMinut
receivedOn, "[email protected]", toAdress, messageId);
}

private static ZonedDateTime getUtcNow() {
return ZonedDateTime.now(ZoneId.of("UTC"));
}

public static Email prepareEmail(
EmailAttachment emailAttachment,
EmailContent emailContent,
String subject,
String rawData,
LocalDateTime receivedOn,
ZonedDateTime receivedOn,
String fromAddress,
String toAdress,
String messageId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
import org.springframework.test.web.servlet.MockMvc;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import static org.hamcrest.MatcherAssert.*;
Expand Down Expand Up @@ -375,15 +376,15 @@ void shouldSearchEmailsByToAddressContainsOrSubjectEqual() throws Exception {

@Test
void shouldSearchEmailsByReceivedOnGreaterThanOrEqualDates() throws Exception {
final var now = LocalDateTime.now();
final var now = getUtcNow();
final var startDate = now.minusMinutes(1);

createRandomEmails(5, 5);
final var email1 = createRandomEmail(0);
createRandomEmails(5, 10);
createRandomEmails(5, 15);

final var formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
final var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
final var greaterThanOrEqualExpression = new GreaterThanOrEqualExpression("receivedOn", startDate.format(formatter));
final var searchRequest = SearchRequest.of(greaterThanOrEqualExpression);

Expand All @@ -402,15 +403,15 @@ void shouldSearchEmailsByReceivedOnGreaterThanOrEqualDates() throws Exception {

@Test
void shouldSearchEmailsByReceivedOnGreaterThanDates() throws Exception {
final var now = LocalDateTime.now();
final var now = getUtcNow();
final var startDate = now.minusMinutes(1);

createRandomEmails(5, 5);
final var email1 = createRandomEmail(0);
createRandomEmails(5, 10);
createRandomEmails(5, 15);

final var formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
final var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
final var greaterThanExpression = new GreaterThanExpression("receivedOn", startDate.format(formatter));
final var searchRequest = SearchRequest.of(greaterThanExpression);

Expand All @@ -429,15 +430,15 @@ void shouldSearchEmailsByReceivedOnGreaterThanDates() throws Exception {

@Test
void shouldSearchEmailsByReceivedOnLessThanDates() throws Exception {
final var now = LocalDateTime.now();
final var now = getUtcNow();
final var endDate = now.minusMinutes(20);

createRandomEmails(5, 5);
createRandomEmails(5, 10);
final var email1 = createRandomEmail(25);
createRandomEmails(5, 15);

final var formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
final var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
final var lessThanExpression = new LessThanExpression("receivedOn", endDate.format(formatter));
final var searchRequest = SearchRequest.of(lessThanExpression);

Expand All @@ -455,17 +456,21 @@ void shouldSearchEmailsByReceivedOnLessThanDates() throws Exception {
assertEquals(List.of(email1), emailSearchResult.getContent());
}

private static ZonedDateTime getUtcNow() {
return ZonedDateTime.now(ZoneId.of("UTC"));
}

@Test
void shouldSearchEmailsByReceivedOnLessThanOrEqualDates() throws Exception {
final var now = LocalDateTime.now();
final var now = getUtcNow();
final var endDate = now.minusMinutes(20);

createRandomEmails(5, 5);
createRandomEmails(5, 10);
final var email1 = createRandomEmail(25);
createRandomEmails(5, 15);

final var formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
final var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
final var lessThanOrEqualExpression = new LessThanOrEqualExpression("receivedOn", endDate.format(formatter));
final var searchRequest = SearchRequest.of(lessThanOrEqualExpression);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import org.springframework.test.context.junit.jupiter.SpringExtension;

import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.contains;
Expand Down Expand Up @@ -76,7 +77,7 @@ void shouldNotDeleteAnyEmailWhenTheNumberOfEmailsDoesNotExceedTheRetentionLimitO

private Email createRandomEmail(int minusMinutes) {
var randomToken = RandomStringUtils.randomAlphanumeric(6);
var receivedOn = LocalDateTime.now().minusMinutes(minusMinutes);
var receivedOn = ZonedDateTime.now(ZoneId.of("UTC")).minusMinutes(minusMinutes);

var content = new EmailContent();
content.setContentType(ContentType.PLAIN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import org.mockito.junit.jupiter.MockitoExtension;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import static java.util.stream.Collectors.toList;
import static org.hamcrest.MatcherAssert.*;
Expand All @@ -38,7 +39,7 @@ class EmailFactoryTest {
@ParameterizedTest
@ValueSource(strings = {"mail-with-subject.eml", "mail-with-subject-without-content-type.eml", "multipart-mail-plain-only.eml"})
void shouldCreateMailPlainTextEmails(String testFilename) throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
var rawData = new RawData(SENDER, RECEIVER, data);
Expand All @@ -50,7 +51,7 @@ void shouldCreateMailPlainTextEmails(String testFilename) throws Exception {
assertPlainTextEmail(now, dataAsString, result);
}

private void assertPlainTextEmail(LocalDateTime now, String dataAsString, Email result) {
private void assertPlainTextEmail(ZonedDateTime now, String dataAsString, Email result) {
assertEquals(SENDER, result.getFromAddress());
assertEquals(RECEIVER, result.getToAddress());
assertEquals("This is the mail title", result.getSubject());
Expand All @@ -65,7 +66,7 @@ private void assertPlainTextEmail(LocalDateTime now, String dataAsString, Email

@Test
void shouldCreateEmailForEmlFileWithSubjectAndContentTypeHtml() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "mail-with-subject-and-content-type-html.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand All @@ -89,7 +90,7 @@ void shouldCreateEmailForEmlFileWithSubjectAndContentTypeHtml() throws Exception

@Test
void shouldCreateEmailForEmlFileWithSubjectAndContentTypeHtmlAndEmbeddedImage() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "mail-with-subect-and-content-type-html-with-inline-image.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand Down Expand Up @@ -118,7 +119,7 @@ void shouldCreateEmailForEmlFileWithSubjectAndContentTypeHtmlAndEmbeddedImage()

@Test
void shouldThrowExceptionWhenInlineImageIsBroken() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "mail-with-subect-and-content-type-html-with-broken-inline-image.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var rawData = new RawData(SENDER, RECEIVER, data);
Expand All @@ -134,7 +135,7 @@ void shouldThrowExceptionWhenInlineImageIsBroken() throws Exception {

@Test
void shouldCreateEmailForEmlFileWithoutSubjectAndContentTypePlain() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "mail-without-subject.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand All @@ -158,7 +159,7 @@ void shouldCreateEmailForEmlFileWithoutSubjectAndContentTypePlain() throws Excep

@Test
void shouldCreateMailForPlainText() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var dataAsString = "this is just some dummy content";
var data = dataAsString.getBytes(StandardCharsets.UTF_8);
var rawData = new RawData(SENDER, RECEIVER, data);
Expand All @@ -181,7 +182,7 @@ void shouldCreateMailForPlainText() throws Exception {

@Test
void shouldCreateMailForMultipartWithContentTypeHtmlAndPlain() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "multipart-mail.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand All @@ -206,7 +207,7 @@ void shouldCreateMailForMultipartWithContentTypeHtmlAndPlain() throws Exception

@Test
void shouldCreateMailForMultipartWithUnknownContentType() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "multipart-mail-unknown-content-type.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand All @@ -231,7 +232,7 @@ void shouldCreateMailForMultipartWithUnknownContentType() throws Exception {

@Test
void shouldCreateMailForMultipartWithPlainAndHtmlContentAndAttachments() throws Exception {
var now = LocalDateTime.now();
var now = getUtcNow();
var testFilename = "multipart-mail-html-and-plain-with-attachments.eml";
var data = TestResourceUtil.getTestFileContentBytes(testFilename);
var dataAsString = new String(data, StandardCharsets.UTF_8);
Expand All @@ -253,6 +254,10 @@ void shouldCreateMailForMultipartWithPlainAndHtmlContentAndAttachments() throws
assertEquals(now, result.getReceivedOn());
assertThat(result.getAttachments(), hasSize(2));
assertThat(result.getAttachments().stream().map(EmailAttachment::getFilename).collect(toList()), containsInAnyOrder("customizing.css", "app-icon.png"));
}
}

private static ZonedDateTime getUtcNow() {
return ZonedDateTime.now(ZoneId.of("UTC"));
}

}

0 comments on commit 6a556a5

Please sign in to comment.