MailDateFormatTest.java
/*
* Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.mail.internet;
import org.junit.Test;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Test MailDateFormat: formatting and parsing of dates as specified by
* <a href="http://www.ietf.org/rfc/rfc2822.txt">RFC 2822</a>.
*
* @author Anthony Vanelverdinghe
*/
public class MailDateFormatTest {
/*
* Formatting + parsing
*/
@Test
public void mustSucceedRoundTrip() throws ParseException {
Date date = new Date(1341100798000L); // milliseconds must be 0
SimpleDateFormat fmt = getDefault();
assertThat(fmt.parse(fmt.format(date)), is(date));
}
/*
* Formatting
*/
@Test()
public void formatMustThrowNpeForNullArgs() {
for (int mask = 0; mask < 7; mask++) {
try {
Date date = (mask & 1) == 1
? new Date() : null;
StringBuffer toAppendTo = (mask & 2) == 2
? new StringBuffer() : null;
FieldPosition pos = (mask & 4) == 4
? new FieldPosition(0) : null;
getDefault().format(date, toAppendTo, pos);
fail("No NullPointerException thrown for mask = " + mask);
} catch (NullPointerException e) {
}
}
}
@Test
public void mustFormatRfc2822WithOptionalCfws() {
Date date = mustPass(getDefault(), "1 Jan 2015 00:00 +0000");
assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)");
}
@Test
public void mustUseTimeZoneForFormatting() {
String input = "1 Jan 2015 00:00 +0100";
SimpleDateFormat fmt = getDefault();
Date date = mustPass(fmt, input);
fmt.setTimeZone(TimeZone.getTimeZone("Etc/GMT+8"));
assertThat(fmt.format(date),
is("Wed, 31 Dec 2014 15:00:00 -0800 (GMT-08:00)"));
}
/*
* Parsing
*/
@Test
public void parseMustThrowNpeForNullArgs() {
for (int mask = 0; mask < 3; mask++) {
try {
String source = (mask & 1) == 1
? "1 Jan 2015 00:00 +0100" : null;
ParsePosition pos = (mask & 2) == 2
? new ParsePosition(0) : null;
getDefault().parse(source, pos);
fail("No NullPointerException thrown for mask = " + mask);
} catch (NullPointerException e) {
}
}
}
@Test
public void mustReturnOnInvalidParsePosition() {
assertNull(getDefault().parse("", new ParsePosition(-1)));
assertNull(getDefault().parse("", new ParsePosition(0)));
assertNull(getDefault().parse("", new ParsePosition(1)));
}
@Test
public void mustParseRfc2822() {
mustPass(getDefault(), "Thu, 1 Jan 2015 00:00:00 +0000");
mustFail(getStrict(), "foo", 0);
mustFail(getLenient(), "foo", 3);
}
@Test
public void mustRetainTimeZoneForBackwardCompatibility() {
// though java.text.DateFormat::parse(String, ParsePosition) specifies:
// "This parsing operation uses the calendar to produce a Date. As a
// result, the calendar's date-time fields and the TimeZone value
// may have been overwritten, depending on subclass implementations.
// Any TimeZone value that has previously been set by a call to
// setTimeZone may need to be restored for further operations."
// we must retain the TimeZone value for backward compatibility reasons
SimpleDateFormat fmt = getDefault();
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Brussels"));
mustPass(fmt, "1 Jan 2015 00:00 +0000");
assertThat(fmt.getTimeZone(),
is(TimeZone.getTimeZone("Europe/Brussels")));
}
@Test
public void mustUseLeniencyForParsing() {
String lenientInput = "1-Jan-2015 00:00 +0000";
String strictInput = "1 Jan 2015 00:00 +0000";
SimpleDateFormat fmt = getStrict();
mustFail(fmt, lenientInput, 1);
mustPass(fmt, strictInput);
fmt.setLenient(true);
mustPass(fmt, lenientInput);
mustPass(fmt, strictInput);
fmt.setLenient(false);
mustFail(fmt, lenientInput, 1);
mustPass(fmt, strictInput);
}
@Test
public void mustReportCorrectErrorIndex() {
mustFail(getDefault(), "1 Juk 2015 00:00 +0000", 2);
mustFail(getStrict(), "1\r\nJan 2015 00:00 +0000", 1);
mustFail(getStrict(), "1 Jan 201 00:00 +0000", 6);
}
@Test
public void lenientMustBeBackwardCompatible() {
mustPass(getLenient(), "Thu, 1 Jan 2015 12:00 +0000");
mustPass(getLenient(), "32 dEC 2015 12:00 +0099");
mustPass(getLenient(), "fooFri,999-Jan-99999999 99:99:99+2399");
mustPass(getLenient(), "fooFri,999-Jan-99999999 99:99:99+2399");
mustPass(getLenient(), "fooFri ,999Jan99999999 99:99:99+2399");
mustPass(getLenient(), "foo31-mAY-1\r3:3:3+");
}
@Test
public void strictMustRejectInvalidBegin() {
mustFail(getStrict(), "foo1 Jan 2015 00:00 +0000", 0);
}
@Test
public void strictMustRejectSemanticallyInvalidDate() {
mustFail(getStrict(), "Fri, 1 Jan 2015 00:00 +0000", 27);
}
/*
* Parsing - range constraints
*/
@Test
public void mustRejectFieldsWithTooManyDigits() {
mustFail(getDefault(), "1234 Jan 2015 00:00 +0000", 0);
mustFail(getDefault(), "1 Jan 123456789 00:00 +0000", 6);
mustFail(getDefault(), "1 Jan 2015 123:00 +0000", 11);
mustFail(getDefault(), "1 Jan 2015 00:123 +0000", 14);
mustFail(getDefault(), "1 Jan 2015 00:00:123 +0000", 17);
}
@Test
public void mustCountLeadingZeroesForTooManyDigits() {
mustFail(getDefault(), "0000000000001 Jan 2015 00:00 +0000", 0);
}
/*
* Parsing - overflow
*/
@Test
public void mustFailFastIfDayContainsTooManyDigits() {
mustFail(getLenient(),
"100000000000000000000000000000001 Jan 2015 00:00 +0000", 0);
}
@Test
public void mustFailFastIfYearContainsTooManyDigits() {
mustFail(getLenient(),
"1 Jan 100000000000000000000000000000002015 00:00 +0000", 6);
}
@Test
public void mustFailIfYearIsTooBigForGregorianCalendar() {
int tooBig = 999999999;
assertTrue(tooBig > new GregorianCalendar().getMaximum(Calendar.YEAR));
mustFail(getLenient(), "1 Jan " + tooBig + " 00:00 +0000", 6);
}
@Test
public void mustFailOnIntegerOverflow() {
mustFail(getLenient(), "456378941961 Jan 2015 00:00 +0000", 0);
}
/*
* Parsing - FWS (folding white space)
*/
@Test
public void mustFailOrSkipCfws() {
String input = "(3 Jan 2015 00:00 +0000) 1 Jan 2015 00:00 +0000";
try {
// NOTE this test fails with getLenient(),
// since lenient parsing must remain backward compatible
Date date = getStrict().parse(input);
assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)");
} catch (ParseException ignored) {
assertTrue("Not supporting CFWS is allowed", true);
}
}
@Test
public void lenientMustAcceptEmptyFwsIffUnambiguous() {
mustPass(getLenient(), "Thu,1Jan2015 12:00+0000");
mustFail(getLenient(), "1 Jan 1015123456:00 +0000", 6);
}
@Test
public void lenientMustRejectInconsistentMonthFws() {
mustFail(getLenient(), "1-Jan 2015 00:00 +0000", 5);
}
@Test
public void strictMustRejectInvalidFws() {
mustFail(getStrict(), "\r\n\r\n 1 Jan 2015 00:00 +0000", 0);
mustFail(getStrict(), "\r\n1 Jan 2015 00:00 +0000", 0);
mustFail(getStrict(), "\n 1 Jan 2015 00:00 +0000", 0);
mustFail(getStrict(), " \n 1 Jan 2015 00:00 +0000", 1);
mustFail(getStrict(), " \r\n1 Jan 2015 00:00 +0000", 0);
}
/*
* Parsing - day-name and month-name
*/
@Test
public void mustAcceptValidDayMonthNames() {
SimpleDateFormat fmt = getDefault();
for (String dayMonth : new String[]{
"Mon, 5 Jan", "Tue, 3 Feb", "Wed, 4 Mar", "Thu, 2 Apr",
"Fri, 1 May", "Sat, 6 Jun", "Sun, 5 Jul", "Sat, 1 Aug",
"Tue, 1 Sep", "Thu, 1 Oct", "Sun, 1 Nov", "Tue, 1 Dec"
}) {
mustPass(fmt, dayMonth + " 2015 00:00:00 +0000");
}
}
@Test
public void strictMustMatchDayNameCaseSensitive() {
mustFail(getStrict(), "tHU, 1 Jan 2015 00:00 +0000", 0);
}
@Test
public void strictMustMatchMonthNameCaseSensitive() {
mustFail(getStrict(), "1 jAN 2015 00:00 +0000", 2);
}
/*
* Parsing - year
*/
@Test
public void lenientMustAcceptSingleDigitYears() {
mustPass(getLenient(), "1 Jan 1 00:00 +0000");
}
@Test
public void lenientMustAcceptYearsBetween1000and1899() {
mustPass(getLenient(), "1 Jan 999 00:00 +0000");
mustPass(getLenient(), "1 Jan 1000 00:00 +0000");
mustPass(getLenient(), "1 Jan 1899 00:00 +0000");
mustPass(getStrict(), "1 Jan 1900 00:00 +0000");
}
@Test
public void lenientMustAdd1900To3DigitYear() {
Date date = mustPass(getLenient(), "1 Jul 900 00:00 +0000");
assertThatDate(date, "Sat, 1 Jul 2800 00:00:00 +0000 (UTC)");
}
@Test
public void strictMustRejectAllYearsLt1900() {
mustFail(getStrict(), "1 Jan 000001 00:00 +0000", 6);
mustFail(getStrict(), "1 Jan 999 00:00 +0000", 6);
mustFail(getStrict(), "1 Jan 1000 00:00 +0000", 6);
mustFail(getStrict(), "1 Jan 1899 00:00 +0000", 6);
mustPass(getStrict(), "1 Jan 1900 00:00 +0000");
}
/*
* Parsing - second
*/
@Test
public void mustParseLeapSecondAsJsr310() {
// JSR-310 replaces 60 with 59
Date date = mustPass(getDefault(), "30 Jun 2012 23:59:60 +0000");
// Date.from(ISO_INSTANT.parse("2012-06-30T23:59:60Z", Instant::from))
assertThat(date, is(new Date(1341100799000L)));
}
/*
* Parsing - zone
*/
@Test
public void mustParseNegativeZeroesZoneAsUtc() {
Date date = mustPass(getDefault(), "1 Jan 2015 00:00 -0000");
assertThatDate(date, "Thu, 1 Jan 2015 00:00:00 +0000 (UTC)");
}
@Test
public void mustCorrectlyParseInputWithTrailingDigits() {
Date date = mustPass(getStrict(), "1 Jan 2015 00:00 -001530");
assertThatDate(date, "Thu, 1 Jan 2015 00:15:00 +0000 (UTC)");
}
@Test
public void mustAcceptZoneWithExtremeOffset() {
Date date = mustPass(getStrict(), "1 Jan 2015 00:00 +9959");
Date equivalentWithoutOffset = mustPass(getStrict(),
"27 Dec 2014 20:01 +0000");
assertThat(date, is(equivalentWithoutOffset));
date = mustPass(getStrict(), "1 Jan 2015 00:00 -9959");
equivalentWithoutOffset = mustPass(getStrict(),
"5 Jan 2015 03:59 +0000");
assertThat(date, is(equivalentWithoutOffset));
}
@Test
public void lenientMustAcceptNonMilitaryZoneNames() {
SimpleDateFormat fmt = getLenient();
for (String zoneName : new String[]{"GMT", "UT",
"EDT", "EST", "CDT", "CST", "MDT", "MST", "PDT", "PST"}) {
mustPass(fmt, "1 Jan 2015 00:00 " + zoneName);
}
}
@Test
public void strictMustMatchZone() {
mustFail(getStrict(), "1 Jan 2015 00:00", 16);
}
@Test
public void strictMustRejectZoneOffsetMinutesGreaterThan60() {
mustFail(getStrict(), "1 Jan 2015 00:00 +0060", 17);
}
/*
* Unsupported methods. When possible, the test also demonstrates
* why invoking the method must be prohibited.
*/
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitSetCalendar() {
getDefault().setCalendar(Calendar.getInstance());
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitSetNumberFormat() {
getDefault().setNumberFormat(NumberFormat.getInstance());
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitApplyLocalizedPattern() {
SimpleDateFormat fmt = getStrict();
fmt.applyLocalizedPattern("yyyy");
Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000");
assertThat(fmt.format(date), is("2015"));
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitApplyPattern() {
SimpleDateFormat fmt = getStrict();
fmt.applyPattern("yyyy");
Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000");
assertThat(fmt.format(date), is("2015"));
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitGet2DigitYearStart() {
getDefault().get2DigitYearStart();
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitSet2DigitYearStart() {
getDefault().set2DigitYearStart(new Date());
}
@Test(expected = UnsupportedOperationException.class)
public void mustProhibitSetDateFormatSymbols() {
SimpleDateFormat fmt = getStrict();
fmt.setDateFormatSymbols(new DateFormatSymbols(Locale.FRENCH));
Date date = mustPass(fmt, "1 Jan 2015 00:00:00 +0000");
assertThatDate(date, "jeu., 1 janv. 2015 00:00:00 +0000 (UTC)");
}
/*
* Helper methods
*/
private static SimpleDateFormat getDefault() {
return new MailDateFormat();
}
private static SimpleDateFormat getLenient() {
SimpleDateFormat fmt = getDefault();
fmt.setLenient(true);
return fmt;
}
private static SimpleDateFormat getStrict() {
SimpleDateFormat fmt = getDefault();
fmt.setLenient(false);
return fmt;
}
private void assertThatDate(Date date, String formattedDate) {
SimpleDateFormat fmt = getDefault();
fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
assertThat(fmt.format(date), is(formattedDate));
}
private Date mustPass(DateFormat fmt, String input) {
try {
Date date = fmt.parse(input);
assertNotNull(date);
return date;
} catch (ParseException e) {
fail(String.format("'%s' is a valid date in %s mode", input,
fmt.isLenient() ? "lenient" : "strict"));
throw new AssertionError();
}
}
private void mustFail(DateFormat fmt, String input, int errorOffset) {
try {
Date result = fmt.parse(input);
fail(String.format("'%s' is not a valid date in %s mode", input,
fmt.isLenient() ? "lenient" : "strict"));
} catch (ParseException e) {
assertThat(e.getErrorOffset(), is(errorOffset));
}
}
}