use google java formatter to avoid downloading eclipse during build

This commit is contained in:
Athou
2026-07-05 10:18:30 +02:00
parent f4dc279ec9
commit 1bd9b71215
200 changed files with 13923 additions and 12754 deletions

View File

@@ -5,14 +5,14 @@ import org.junit.jupiter.api.Test;
class DigestsTest {
@Test
void sha1Hex() {
Assertions.assertEquals("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", Digests.sha1Hex("hello"));
}
@Test
void sha1Hex() {
Assertions.assertEquals(
"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", Digests.sha1Hex("hello"));
}
@Test
void md5Hex() {
Assertions.assertEquals("5d41402abc4b2a76b9719d911017c592", Digests.md5Hex("hello"));
}
}
@Test
void md5Hex() {
Assertions.assertEquals("5d41402abc4b2a76b9719d911017c592", Digests.md5Hex("hello"));
}
}

View File

@@ -1,5 +1,14 @@
package com.commafeed.backend;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter.HttpResponseException;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.google.common.net.HttpHeaders;
import io.quarkus.runtime.configuration.MemorySize;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -15,7 +24,6 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.hc.client5.http.ConnectTimeoutException;
import org.apache.hc.core5.http.HttpStatus;
@@ -35,387 +43,508 @@ import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.mockserver.model.MediaType;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter.HttpResponseException;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.HttpGetter.TooManyRequestsException;
import com.google.common.net.HttpHeaders;
import io.quarkus.runtime.configuration.MemorySize;
class HttpGetterTest {
private static final Instant NOW = Instant.now();
private MockServerClient mockServerClient;
private String feedUrl;
private byte[] feedContent;
private CommaFeedConfiguration config;
private HttpClientFactory provider;
private HttpGetter getter;
@BeforeEach
void init() throws IOException {
this.mockServerClient = ClientAndServer.startClientAndServer(0);
this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/";
this.feedContent = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/feed/rss.xml")));
this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test"));
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().maxResponseSize()).thenReturn(new MemorySize(new BigInteger("10000")));
Mockito.when(config.httpClient().cache().enabled()).thenReturn(true);
Mockito.when(config.httpClient().cache().maximumMemorySize()).thenReturn(new MemorySize(new BigInteger("100000")));
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
this.provider = new HttpClientFactory(config, Mockito.mock(CommaFeedVersion.class));
this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@AfterEach
void tearDown() {
if (this.mockServerClient != null) {
this.mockServerClient.stop();
}
}
@ParameterizedTest
@ValueSource(
ints = { HttpStatus.SC_UNAUTHORIZED, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_NOT_FOUND, HttpStatus.SC_INTERNAL_SERVER_ERROR })
void errorCodes(int code) {
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withStatusCode(code));
HttpResponseException e = Assertions.assertThrows(HttpResponseException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(code, e.getCode());
}
@Test
void validFeed() throws Exception {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withBody(feedContent)
.withContentType(MediaType.APPLICATION_ATOM_XML)
.withHeader(HttpHeaders.LAST_MODIFIED, "123456")
.withHeader(HttpHeaders.ETAG, "78910")
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate")
.withHeader(HttpHeaders.RETRY_AFTER, "120"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertArrayEquals(feedContent, result.content());
Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.contentType());
Assertions.assertEquals("123456", result.lastModifiedSince());
Assertions.assertEquals("78910", result.eTag());
Assertions.assertEquals(Duration.ofSeconds(60), result.validFor());
Assertions.assertEquals(this.feedUrl, result.urlAfterRedirect());
}
@Test
void ignoreInvalidCacheControlValue() throws Exception {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withBody(feedContent)
.withContentType(MediaType.APPLICATION_ATOM_XML)
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=60; must-revalidate"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals(Duration.ZERO, result.validFor());
}
@Test
void tooManyRequestsExceptionSeconds() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response().withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS).withHeader(HttpHeaders.RETRY_AFTER, "120"));
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter());
}
@Test
void tooManyRequestsExceptionDate() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS)
.withHeader(HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT"));
TooManyRequestsException e = Assertions.assertThrows(TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter());
}
@ParameterizedTest
@ValueSource(
ints = { HttpStatus.SC_MOVED_PERMANENTLY, HttpStatus.SC_MOVED_TEMPORARILY, HttpStatus.SC_TEMPORARY_REDIRECT,
HttpStatus.SC_PERMANENT_REDIRECT })
void followRedirects(int code) throws Exception {
// first redirect
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/"))
.respond(HttpResponse.response()
.withStatusCode(code)
.withHeader(HttpHeaders.LOCATION, "http://localhost:" + this.mockServerClient.getPort() + "/redirected"));
// second redirect
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/redirected"))
.respond(HttpResponse.response()
.withStatusCode(code)
.withHeader(HttpHeaders.LOCATION, "http://localhost:" + this.mockServerClient.getPort() + "/redirected-2"));
// final destination
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/redirected-2"))
.respond(HttpResponse.response().withBody(feedContent).withContentType(MediaType.APPLICATION_ATOM_XML));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals("http://localhost:" + this.mockServerClient.getPort() + "/redirected-2", result.urlAfterRedirect());
}
@Test
void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl));
}
@Test
void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
this.getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
// try to connect to a non-routable address
// https://stackoverflow.com/a/904609
Exception e = Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1"));
Assertions.assertTrue(e instanceof ConnectTimeoutException
// A NoRouteToHostException can also be thrown in some cases
// depending on the underlying network configuration
// https://github.com/Athou/commafeed/issues/1876
|| e instanceof NoRouteToHostException,
"Expected ConnectTimeoutException or NoRouteToHostException, but got: " + e.getClass().getName());
}
@Test
void userAgent() throws Exception {
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.USER_AGENT, "http-getter-test"))
.respond(HttpResponse.response().withBody("ok"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals("ok", new String(result.content()));
}
@Test
void lastModifiedReturns304() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456"))
.respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(NotModifiedException.class,
() -> getter.get(HttpGetter.HttpRequest.builder(this.feedUrl).lastModified("123456").build()));
}
@Test
void eTagReturns304() {
this.mockServerClient.when(HttpRequest.request().withMethod("GET").withHeader(HttpHeaders.IF_NONE_MATCH, "78910"))
.respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(NotModifiedException.class,
() -> getter.get(HttpGetter.HttpRequest.builder(this.feedUrl).eTag("78910").build()));
}
@Test
void ignoreCookie() {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
calls.incrementAndGet();
if (req.containsHeader(HttpHeaders.COOKIE)) {
throw new Exception("cookie should not be sent by the client");
}
return HttpResponse.response().withBody("ok").withHeader(HttpHeaders.SET_COOKIE, "foo=bar");
});
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar"));
Assertions.assertEquals(2, calls.get());
}
@Test
void cacheSubsequentCalls() throws Exception {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
calls.incrementAndGet();
return HttpResponse.response().withBody("ok");
});
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals(result, getter.get(this.feedUrl));
Assertions.assertEquals(1, calls.get());
}
@Test
void largeFeedWithContentLengthHeader() {
byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody(bytes));
IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals("Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)", e.getMessage());
}
@Test
void largeFeedWithoutContentLengthHeader() {
byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response()
.withBody(bytes)
.withConnectionOptions(ConnectionOptions.connectionOptions().withSuppressContentLengthHeader(true)));
IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals("Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage());
}
@Test
void ignoreInvalidSsl() throws Exception {
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(HttpResponse.response().withBody("ok"));
HttpResult result = getter.get("https://localhost:" + this.mockServerClient.getPort());
Assertions.assertEquals("ok", new String(result.content()));
}
@Test
void doesNotUseUpgradeProtocolHeader() {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
calls.incrementAndGet();
if (req.containsHeader(HttpHeaders.UPGRADE)) {
throw new Exception("upgrade header should not be sent by the client");
}
return HttpResponse.response().withBody("ok");
});
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
Assertions.assertEquals(1, calls.get());
}
@Nested
class Compression {
private static final String ACCEPT_ENCODING = "gzip, deflate, br";
@Test
void gzip() throws Exception {
supportsCompression("gzip", GZIPOutputStream::new);
}
@Test
void deflate() throws Exception {
supportsCompression("deflate", DeflaterOutputStream::new);
}
void supportsCompression(String encoding, CompressionFunction compressionFunction) throws Exception {
String body = "my body";
HttpGetterTest.this.mockServerClient.when(HttpRequest.request().withMethod("GET")).respond(req -> {
String acceptEncodingHeader = req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
throw new Exception("Wrong value in the Accept-Encoding header, should be '%s' but was '%s'".formatted(ACCEPT_ENCODING,
acceptEncodingHeader));
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (OutputStream compressionOutputStream = compressionFunction.apply(output)) {
compressionOutputStream.write(body.getBytes());
}
return HttpResponse.response().withBody(output.toByteArray()).withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
});
HttpResult result = getter.get(HttpGetterTest.this.feedUrl);
Assertions.assertEquals(body, new String(result.content()));
}
@FunctionalInterface
public interface CompressionFunction {
OutputStream apply(OutputStream input) throws IOException;
}
}
@Nested
class SchemeNotAllowed {
@Test
void file() {
Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("file://localhost"));
}
@Test
void ftp() {
Assertions.assertThrows(HttpGetter.SchemeNotAllowedException.class, () -> getter.get("ftp://localhost"));
}
}
@Nested
class HostNotAllowed {
@BeforeEach
void init() {
Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true);
getter = new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@Test
void localhost() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://localhost"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://127.0.0.1"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://2130706433"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
}
@Test
void zero() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0.0.0.0"));
}
@Test
void linkLocal() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.12.34"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://169.254.169.254"));
}
@Test
void multicast() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://224.2.3.4"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://239.255.255.254"));
}
@Test
void privateIpv4Ranges() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://10.0.0.1"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://172.16.0.1"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://192.168.0.1"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://100.64.0.1"));
}
@Test
void privateIpv6Ranges() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://[fe80::215:5dff:fe15:102]"));
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]"));
}
}
}
private static final Instant NOW = Instant.now();
private MockServerClient mockServerClient;
private String feedUrl;
private byte[] feedContent;
private CommaFeedConfiguration config;
private HttpClientFactory provider;
private HttpGetter getter;
@BeforeEach
void init() throws IOException {
this.mockServerClient = ClientAndServer.startClientAndServer(0);
this.feedUrl = "http://localhost:" + this.mockServerClient.getPort() + "/";
this.feedContent =
IOUtils.toByteArray(
Objects.requireNonNull(getClass().getResource("/feed/rss.xml")));
this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.httpClient().userAgent()).thenReturn(Optional.of("http-getter-test"));
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().sslHandshakeTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().socketTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().connectionTimeToLive()).thenReturn(Duration.ofSeconds(30));
Mockito.when(config.httpClient().maxResponseSize())
.thenReturn(new MemorySize(new BigInteger("10000")));
Mockito.when(config.httpClient().cache().enabled()).thenReturn(true);
Mockito.when(config.httpClient().cache().maximumMemorySize())
.thenReturn(new MemorySize(new BigInteger("100000")));
Mockito.when(config.httpClient().cache().expiration()).thenReturn(Duration.ofMinutes(1));
Mockito.when(config.feedRefresh().httpThreads()).thenReturn(3);
this.provider = new HttpClientFactory(config, Mockito.mock(CommaFeedVersion.class));
this.getter =
new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@AfterEach
void tearDown() {
if (this.mockServerClient != null) {
this.mockServerClient.stop();
}
}
@ParameterizedTest
@ValueSource(
ints = {
HttpStatus.SC_UNAUTHORIZED,
HttpStatus.SC_FORBIDDEN,
HttpStatus.SC_NOT_FOUND,
HttpStatus.SC_INTERNAL_SERVER_ERROR
})
void errorCodes(int code) {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withStatusCode(code));
HttpResponseException e =
Assertions.assertThrows(
HttpResponseException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(code, e.getCode());
}
@Test
void validFeed() throws Exception {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response()
.withBody(feedContent)
.withContentType(MediaType.APPLICATION_ATOM_XML)
.withHeader(HttpHeaders.LAST_MODIFIED, "123456")
.withHeader(HttpHeaders.ETAG, "78910")
.withHeader(
HttpHeaders.CACHE_CONTROL, "max-age=60, must-revalidate")
.withHeader(HttpHeaders.RETRY_AFTER, "120"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertArrayEquals(feedContent, result.content());
Assertions.assertEquals(MediaType.APPLICATION_ATOM_XML.toString(), result.contentType());
Assertions.assertEquals("123456", result.lastModifiedSince());
Assertions.assertEquals("78910", result.eTag());
Assertions.assertEquals(Duration.ofSeconds(60), result.validFor());
Assertions.assertEquals(this.feedUrl, result.urlAfterRedirect());
}
@Test
void ignoreInvalidCacheControlValue() throws Exception {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response()
.withBody(feedContent)
.withContentType(MediaType.APPLICATION_ATOM_XML)
.withHeader(
HttpHeaders.CACHE_CONTROL, "max-age=60; must-revalidate"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals(Duration.ZERO, result.validFor());
}
@Test
void tooManyRequestsExceptionSeconds() {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response()
.withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS)
.withHeader(HttpHeaders.RETRY_AFTER, "120"));
TooManyRequestsException e =
Assertions.assertThrows(
TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(NOW.plusSeconds(120), e.getRetryAfter());
}
@Test
void tooManyRequestsExceptionDate() {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response()
.withStatusCode(HttpStatus.SC_TOO_MANY_REQUESTS)
.withHeader(
HttpHeaders.RETRY_AFTER, "Wed, 21 Oct 2015 07:28:00 GMT"));
TooManyRequestsException e =
Assertions.assertThrows(
TooManyRequestsException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(Instant.parse("2015-10-21T07:28:00Z"), e.getRetryAfter());
}
@ParameterizedTest
@ValueSource(
ints = {
HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY,
HttpStatus.SC_TEMPORARY_REDIRECT,
HttpStatus.SC_PERMANENT_REDIRECT
})
void followRedirects(int code) throws Exception {
// first redirect
this.mockServerClient
.when(HttpRequest.request().withMethod("GET").withPath("/"))
.respond(
HttpResponse.response()
.withStatusCode(code)
.withHeader(
HttpHeaders.LOCATION,
"http://localhost:"
+ this.mockServerClient.getPort()
+ "/redirected"));
// second redirect
this.mockServerClient
.when(HttpRequest.request().withMethod("GET").withPath("/redirected"))
.respond(
HttpResponse.response()
.withStatusCode(code)
.withHeader(
HttpHeaders.LOCATION,
"http://localhost:"
+ this.mockServerClient.getPort()
+ "/redirected-2"));
// final destination
this.mockServerClient
.when(HttpRequest.request().withMethod("GET").withPath("/redirected-2"))
.respond(
HttpResponse.response()
.withBody(feedContent)
.withContentType(MediaType.APPLICATION_ATOM_XML));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals(
"http://localhost:" + this.mockServerClient.getPort() + "/redirected-2",
result.urlAfterRedirect());
}
@Test
void dataTimeout() {
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofMillis(500));
this.getter =
new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl));
}
@Test
void connectTimeout() {
Mockito.when(config.httpClient().connectTimeout()).thenReturn(Duration.ofMillis(500));
this.getter =
new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
// try to connect to a non-routable address
// https://stackoverflow.com/a/904609
Exception e =
Assertions.assertThrows(Exception.class, () -> getter.get("http://10.255.255.1"));
Assertions.assertTrue(
e instanceof ConnectTimeoutException
// A NoRouteToHostException can also be thrown in some cases
// depending on the underlying network configuration
// https://github.com/Athou/commafeed/issues/1876
|| e instanceof NoRouteToHostException,
"Expected ConnectTimeoutException or NoRouteToHostException, but got: "
+ e.getClass().getName());
}
@Test
void userAgent() throws Exception {
this.mockServerClient
.when(
HttpRequest.request()
.withMethod("GET")
.withHeader(HttpHeaders.USER_AGENT, "http-getter-test"))
.respond(HttpResponse.response().withBody("ok"));
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals("ok", new String(result.content()));
}
@Test
void lastModifiedReturns304() {
this.mockServerClient
.when(
HttpRequest.request()
.withMethod("GET")
.withHeader(HttpHeaders.IF_MODIFIED_SINCE, "123456"))
.respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(
NotModifiedException.class,
() ->
getter.get(
HttpGetter.HttpRequest.builder(this.feedUrl)
.lastModified("123456")
.build()));
}
@Test
void eTagReturns304() {
this.mockServerClient
.when(
HttpRequest.request()
.withMethod("GET")
.withHeader(HttpHeaders.IF_NONE_MATCH, "78910"))
.respond(HttpResponse.response().withStatusCode(HttpStatus.SC_NOT_MODIFIED));
Assertions.assertThrows(
NotModifiedException.class,
() ->
getter.get(
HttpGetter.HttpRequest.builder(this.feedUrl)
.eTag("78910")
.build()));
}
@Test
void ignoreCookie() {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
req -> {
calls.incrementAndGet();
if (req.containsHeader(HttpHeaders.COOKIE)) {
throw new Exception("cookie should not be sent by the client");
}
return HttpResponse.response()
.withBody("ok")
.withHeader(HttpHeaders.SET_COOKIE, "foo=bar");
});
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar"));
Assertions.assertEquals(2, calls.get());
}
@Test
void cacheSubsequentCalls() throws Exception {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
req -> {
calls.incrementAndGet();
return HttpResponse.response().withBody("ok");
});
HttpResult result = getter.get(this.feedUrl);
Assertions.assertEquals(result, getter.get(this.feedUrl));
Assertions.assertEquals(1, calls.get());
}
@Test
void largeFeedWithContentLengthHeader() {
byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withBody(bytes));
IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(
"Response size (100000 bytes) exceeds the maximum allowed size (10000 bytes)",
e.getMessage());
}
@Test
void largeFeedWithoutContentLengthHeader() {
byte[] bytes = new byte[100000];
Arrays.fill(bytes, (byte) 1);
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
HttpResponse.response()
.withBody(bytes)
.withConnectionOptions(
ConnectionOptions.connectionOptions()
.withSuppressContentLengthHeader(true)));
IOException e = Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
Assertions.assertEquals(
"Response size exceeds the maximum allowed size (10000 bytes)", e.getMessage());
}
@Test
void ignoreInvalidSsl() throws Exception {
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(HttpResponse.response().withBody("ok"));
HttpResult result = getter.get("https://localhost:" + this.mockServerClient.getPort());
Assertions.assertEquals("ok", new String(result.content()));
}
@Test
void doesNotUseUpgradeProtocolHeader() {
AtomicInteger calls = new AtomicInteger();
this.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
req -> {
calls.incrementAndGet();
if (req.containsHeader(HttpHeaders.UPGRADE)) {
throw new Exception(
"upgrade header should not be sent by the client");
}
return HttpResponse.response().withBody("ok");
});
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
Assertions.assertEquals(1, calls.get());
}
@Nested
class Compression {
private static final String ACCEPT_ENCODING = "gzip, deflate, br";
@Test
void gzip() throws Exception {
supportsCompression("gzip", GZIPOutputStream::new);
}
@Test
void deflate() throws Exception {
supportsCompression("deflate", DeflaterOutputStream::new);
}
void supportsCompression(String encoding, CompressionFunction compressionFunction)
throws Exception {
String body = "my body";
HttpGetterTest.this
.mockServerClient
.when(HttpRequest.request().withMethod("GET"))
.respond(
req -> {
String acceptEncodingHeader =
req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
throw new Exception(
"Wrong value in the Accept-Encoding header, should be '%s' but was '%s'"
.formatted(
ACCEPT_ENCODING, acceptEncodingHeader));
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (OutputStream compressionOutputStream =
compressionFunction.apply(output)) {
compressionOutputStream.write(body.getBytes());
}
return HttpResponse.response()
.withBody(output.toByteArray())
.withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
});
HttpResult result = getter.get(HttpGetterTest.this.feedUrl);
Assertions.assertEquals(body, new String(result.content()));
}
@FunctionalInterface
public interface CompressionFunction {
OutputStream apply(OutputStream input) throws IOException;
}
}
@Nested
class SchemeNotAllowed {
@Test
void file() {
Assertions.assertThrows(
HttpGetter.SchemeNotAllowedException.class,
() -> getter.get("file://localhost"));
}
@Test
void ftp() {
Assertions.assertThrows(
HttpGetter.SchemeNotAllowedException.class,
() -> getter.get("ftp://localhost"));
}
}
@Nested
class HostNotAllowed {
@BeforeEach
void init() {
Mockito.when(config.httpClient().blockLocalAddresses()).thenReturn(true);
getter =
new HttpGetter(config, () -> NOW, provider, Mockito.mock(MetricRegistry.class));
}
@Test
void localhost() {
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://localhost"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://127.0.0.1"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://2130706433"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://0x7F.0x00.0x00.0X01"));
}
@Test
void zero() {
Assertions.assertThrows(UnknownHostException.class, () -> getter.get("http://0.0.0.0"));
}
@Test
void linkLocal() {
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://169.254.12.34"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://169.254.169.254"));
}
@Test
void multicast() {
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://224.2.3.4"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://239.255.255.254"));
}
@Test
void privateIpv4Ranges() {
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://10.0.0.1"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://172.16.0.1"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://192.168.0.1"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://100.64.0.1"));
}
@Test
void privateIpv6Ranges() {
Assertions.assertThrows(
UnknownHostException.class,
() -> getter.get("http://[fe80::215:5dff:fe15:102]"));
Assertions.assertThrows(
UnknownHostException.class, () -> getter.get("http://[fd00:dead:beef::50]"));
}
}
}

View File

@@ -5,81 +5,105 @@ import org.junit.jupiter.api.Test;
class UrlsTest {
@Test
void testNormalization() {
String urla1 = "http://example.com/hello?a=1&b=2";
String urla2 = "http://www.example.com/hello?a=1&b=2";
String urla3 = "http://EXAmPLe.com/HELLo?a=1&b=2";
String urla4 = "http://example.com/hello?b=2&a=1";
String urla5 = "https://example.com/hello?a=1&b=2";
@Test
void testNormalization() {
String urla1 = "http://example.com/hello?a=1&b=2";
String urla2 = "http://www.example.com/hello?a=1&b=2";
String urla3 = "http://EXAmPLe.com/HELLo?a=1&b=2";
String urla4 = "http://example.com/hello?b=2&a=1";
String urla5 = "https://example.com/hello?a=1&b=2";
String urlb1 = "http://ftr.fivefilters.org/makefulltextfeed.php?url=http%3A%2F%2Ffeeds.howtogeek.com%2FHowToGeek&max=10&summary=1";
String urlb2 = "http://ftr.fivefilters.org/makefulltextfeed.php?url=http://feeds.howtogeek.com/HowToGeek&max=10&summary=1";
String urlb1 =
"http://ftr.fivefilters.org/makefulltextfeed.php?url=http%3A%2F%2Ffeeds.howtogeek.com%2FHowToGeek&max=10&summary=1";
String urlb2 =
"http://ftr.fivefilters.org/makefulltextfeed.php?url=http://feeds.howtogeek.com/HowToGeek&max=10&summary=1";
String urlc1 = "http://feeds.feedburner.com/Frandroid";
String urlc2 = "http://feeds2.feedburner.com/frandroid";
String urlc3 = "http://feedproxy.google.com/frandroid";
String urlc4 = "http://feeds.feedburner.com/Frandroid/";
String urlc5 = "http://feeds.feedburner.com/Frandroid?format=rss";
String urlc1 = "http://feeds.feedburner.com/Frandroid";
String urlc2 = "http://feeds2.feedburner.com/frandroid";
String urlc3 = "http://feedproxy.google.com/frandroid";
String urlc4 = "http://feeds.feedburner.com/Frandroid/";
String urlc5 = "http://feeds.feedburner.com/Frandroid?format=rss";
String urld1 = "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds.feedburner.com/Frandroid";
String urld2 = "http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds2.feedburner.com/Frandroid";
String urld1 =
"http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds.feedburner.com/Frandroid";
String urld2 =
"http://fivefilters.org/content-only/makefulltextfeed.php?url=http://feeds2.feedburner.com/Frandroid";
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla2));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla3));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla4));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla5));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla2));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla3));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla4));
Assertions.assertEquals(Urls.normalize(urla1), Urls.normalize(urla5));
Assertions.assertEquals(Urls.normalize(urlb1), Urls.normalize(urlb2));
Assertions.assertEquals(Urls.normalize(urlb1), Urls.normalize(urlb2));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc2));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc3));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc4));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc5));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc2));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc3));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc4));
Assertions.assertEquals(Urls.normalize(urlc1), Urls.normalize(urlc5));
Assertions.assertNotEquals(Urls.normalize(urld1), Urls.normalize(urld2));
Assertions.assertNotEquals(Urls.normalize(urld1), Urls.normalize(urld2));
}
}
@Test
void testToAbsoluteUrl() {
String expected = "http://a.com/blog/entry/1";
@Test
void testToAbsoluteUrl() {
String expected = "http://a.com/blog/entry/1";
// usual cases
Assertions.assertEquals(
expected,
Urls.toAbsolute(
"http://a.com/blog/entry/1", "http://a.com/feed/", "http://a.com/feed/"));
Assertions.assertEquals(
expected,
Urls.toAbsolute(
"http://a.com/blog/entry/1", "http://a.com/feed", "http://a.com/feed"));
// usual cases
Assertions.assertEquals(expected, Urls.toAbsolute("http://a.com/blog/entry/1", "http://a.com/feed/", "http://a.com/feed/"));
Assertions.assertEquals(expected, Urls.toAbsolute("http://a.com/blog/entry/1", "http://a.com/feed", "http://a.com/feed"));
// relative links
Assertions.assertEquals(
expected,
Urls.toAbsolute("../blog/entry/1", "http://a.com/feed/", "http://a.com/feed/"));
Assertions.assertEquals(
expected,
Urls.toAbsolute("../blog/entry/1", "feed.xml", "http://a.com/feed/feed.xml"));
// relative links
Assertions.assertEquals(expected, Urls.toAbsolute("../blog/entry/1", "http://a.com/feed/", "http://a.com/feed/"));
Assertions.assertEquals(expected, Urls.toAbsolute("../blog/entry/1", "feed.xml", "http://a.com/feed/feed.xml"));
// root-relative links
Assertions.assertEquals(
expected, Urls.toAbsolute("/blog/entry/1", "/feed", "http://a.com/feed"));
// root-relative links
Assertions.assertEquals(expected, Urls.toAbsolute("/blog/entry/1", "/feed", "http://a.com/feed"));
// real cases
Assertions.assertEquals(
"https://github.com/erusev/parsedown/releases/tag/1.3.0",
Urls.toAbsolute(
"/erusev/parsedown/releases/tag/1.3.0",
"/erusev/parsedown/releases",
"https://github.com/erusev/parsedown/tags.atom"));
Assertions.assertEquals(
"http://ergoemacs.org/emacs/elisp_all_about_lines.html",
Urls.toAbsolute(
"elisp_all_about_lines.html",
"blog.xml",
"http://ergoemacs.org/emacs/blog.xml"));
// real cases
Assertions.assertEquals("https://github.com/erusev/parsedown/releases/tag/1.3.0", Urls.toAbsolute(
"/erusev/parsedown/releases/tag/1.3.0", "/erusev/parsedown/releases", "https://github.com/erusev/parsedown/tags.atom"));
Assertions.assertEquals("http://ergoemacs.org/emacs/elisp_all_about_lines.html",
Urls.toAbsolute("elisp_all_about_lines.html", "blog.xml", "http://ergoemacs.org/emacs/blog.xml"));
// invalid relative urls
Assertions.assertEquals(
"title:10001280",
Urls.toAbsolute(
"title:10001280",
"https://www.berliner-zeitung.de",
"https://www.berliner-zeitung.de/feed.xml"));
}
// invalid relative urls
Assertions.assertEquals("title:10001280",
Urls.toAbsolute("title:10001280", "https://www.berliner-zeitung.de", "https://www.berliner-zeitung.de/feed.xml"));
@Test
void testRemoveTrailingSlash() {
final String url = "http://localhost/";
final String result = Urls.removeTrailingSlash(url);
Assertions.assertEquals("http://localhost", result);
}
}
@Test
void testRemoveTrailingSlash() {
final String url = "http://localhost/";
final String result = Urls.removeTrailingSlash(url);
Assertions.assertEquals("http://localhost", result);
}
@Test
void testRemoveTrailingSlashLastSlashOnly() {
final String url = "http://localhost//";
final String result = Urls.removeTrailingSlash(url);
Assertions.assertEquals("http://localhost/", result);
}
}
@Test
void testRemoveTrailingSlashLastSlashOnly() {
final String url = "http://localhost//";
final String result = Urls.removeTrailingSlash(url);
Assertions.assertEquals("http://localhost/", result);
}
}

View File

@@ -1,9 +1,10 @@
package com.commafeed.backend.favicon;
import java.time.Duration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import jakarta.ws.rs.core.MediaType;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,67 +13,68 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
@ExtendWith(MockitoExtension.class)
class FacebookFaviconFetcherTest {
@Mock
private HttpGetter httpGetter;
@Mock private HttpGetter httpGetter;
private FacebookFaviconFetcher faviconFetcher;
private FacebookFaviconFetcher faviconFetcher;
@BeforeEach
void init() {
faviconFetcher = new FacebookFaviconFetcher(httpGetter);
}
@BeforeEach
void init() {
faviconFetcher = new FacebookFaviconFetcher(httpGetter);
}
@Test
void testFetchWithValidFacebookUrl() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something?id=validUserId");
@Test
void testFetchWithValidFacebookUrl() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something?id=validUserId");
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://graph.facebook.com/validUserId/picture?type=square&height=16")).thenReturn(httpResult);
HttpResult httpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://graph.facebook.com/validUserId/picture?type=square&height=16"))
.thenReturn(httpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithNonFacebookUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com");
@Test
void testFetchWithNonFacebookUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com");
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
@Test
void testFetchWithFacebookUrlButNoUserId() {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something");
@Test
void testFetchWithFacebookUrlButNoUserId() {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something");
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something?id=validUserId");
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://www.facebook.com/something?id=validUserId");
Mockito.when(httpGetter.get("https://graph.facebook.com/validUserId/picture?type=square&height=16"))
.thenThrow(new RuntimeException("Network error"));
Mockito.when(
httpGetter.get(
"https://graph.facebook.com/validUserId/picture?type=square&height=16"))
.thenThrow(new RuntimeException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}

View File

@@ -1,9 +1,10 @@
package com.commafeed.backend.favicon;
import java.time.Duration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import jakarta.ws.rs.core.MediaType;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,58 +13,55 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
@ExtendWith(MockitoExtension.class)
class FeedFaviconFetcherTest {
@Mock
private HttpGetter httpGetter;
@Mock private HttpGetter httpGetter;
private FeedFaviconFetcher faviconFetcher;
private FeedFaviconFetcher faviconFetcher;
@BeforeEach
void init() {
faviconFetcher = new FeedFaviconFetcher(httpGetter);
}
@BeforeEach
void init() {
faviconFetcher = new FeedFaviconFetcher(httpGetter);
}
@Test
void testFetchWithNullIconUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
@Test
void testFetchWithNullIconUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
@Test
void testFetchWithValidIconUrl() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setIconUrl("https://example.com/icon.png");
@Test
void testFetchWithValidIconUrl() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setIconUrl("https://example.com/icon.png");
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(httpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult httpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(httpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setIconUrl("https://example.com/icon.png");
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setIconUrl("https://example.com/icon.png");
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenThrow(new RuntimeException("Network error"));
Mockito.when(httpGetter.get("https://example.com/icon.png"))
.thenThrow(new RuntimeException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}

View File

@@ -1,9 +1,10 @@
package com.commafeed.backend.favicon;
import java.time.Duration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import jakarta.ws.rs.core.MediaType;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,111 +13,118 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
@ExtendWith(MockitoExtension.class)
class HtmlFaviconFetcherTest {
@Mock
private HttpGetter httpGetter;
@Mock private HttpGetter httpGetter;
private HtmlFaviconFetcher faviconFetcher;
private HtmlFaviconFetcher faviconFetcher;
@BeforeEach
void init() {
faviconFetcher = new HtmlFaviconFetcher(httpGetter);
}
@BeforeEach
void init() {
faviconFetcher = new HtmlFaviconFetcher(httpGetter);
}
@Test
void testFetchWithNullLink() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
@Test
void testFetchWithNullLink() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter);
}
@Test
void testFetchWithValidIconLink() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
@Test
void testFetchWithValidIconLink() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
String html = "<html><head><link rel=\"icon\" href=\"/favicon.png\" /></head><body></body></html>";
HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
String html =
"<html><head><link rel=\"icon\" href=\"/favicon.png\" /></head><body></body></html>";
HttpResult pageResult =
new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenReturn(iconResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenReturn(iconResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithShortcutIconLink() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
@Test
void testFetchWithShortcutIconLink() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
String html = "<html><head><link rel=\"shortcut icon\" href=\"https://example.com/shortcut-favicon.ico\" /></head><body></body></html>";
HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
String html =
"<html><head><link rel=\"shortcut icon\" href=\"https://example.com/shortcut-favicon.ico\" /></head><body></body></html>";
HttpResult pageResult =
new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult iconResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/shortcut-favicon.ico")).thenReturn(iconResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult iconResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/shortcut-favicon.ico"))
.thenReturn(iconResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithNoIconInPage() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
@Test
void testFetchWithNoIconInPage() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
String html = "<html><head></head><body></body></html>";
HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
String html = "<html><head></head><body></body></html>";
HttpResult pageResult =
new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
@Test
void testFetchWithPageFetchException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
@Test
void testFetchWithPageFetchException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
Mockito.when(httpGetter.get("https://example.com")).thenThrow(new RuntimeException("Network error"));
Mockito.when(httpGetter.get("https://example.com"))
.thenThrow(new RuntimeException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
@Test
void testFetchWithIconFetchException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
@Test
void testFetchWithIconFetchException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
feed.setLink("https://example.com");
String html = "<html><head><link rel=\"icon\" href=\"https://example.com/favicon.png\" /></head><body></body></html>";
HttpResult pageResult = new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
Mockito.when(httpGetter.get("https://example.com/favicon.png")).thenThrow(new RuntimeException("Network error"));
String html =
"<html><head><link rel=\"icon\" href=\"https://example.com/favicon.png\" /></head><body></body></html>";
HttpResult pageResult =
new HttpResult(html.getBytes(), "text/html", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com")).thenReturn(pageResult);
Mockito.when(httpGetter.get("https://example.com/favicon.png"))
.thenThrow(new RuntimeException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}

View File

@@ -1,9 +1,10 @@
package com.commafeed.backend.favicon;
import java.time.Duration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
import jakarta.ws.rs.core.MediaType;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,66 +13,64 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.model.Feed;
@ExtendWith(MockitoExtension.class)
class RootFaviconFetcherTest {
@Mock
private HttpGetter httpGetter;
@Mock private HttpGetter httpGetter;
private RootFaviconFetcher faviconFetcher;
private RootFaviconFetcher faviconFetcher;
@BeforeEach
void init() {
faviconFetcher = new RootFaviconFetcher(httpGetter);
}
@BeforeEach
void init() {
faviconFetcher = new RootFaviconFetcher(httpGetter);
}
@Test
void testFetchUsesLinkWhenAvailable() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://feeds.example.com/feed");
feed.setLink("https://www.example.com/blog");
@Test
void testFetchUsesLinkWhenAvailable() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://feeds.example.com/feed");
feed.setLink("https://www.example.com/blog");
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.example.com/favicon.ico")).thenReturn(httpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult httpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.example.com/favicon.ico")).thenReturn(httpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchFallsBackToUrlWhenLinkIsNull() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed.xml");
@Test
void testFetchFallsBackToUrlWhenLinkIsNull() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed.xml");
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult httpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenReturn(httpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/x-icon";
HttpResult httpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenReturn(httpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed.xml");
feed.setLink("https://example.com");
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed.xml");
feed.setLink("https://example.com");
Mockito.when(httpGetter.get("https://example.com/favicon.ico")).thenThrow(new RuntimeException("Network error"));
Mockito.when(httpGetter.get("https://example.com/favicon.ico"))
.thenThrow(new RuntimeException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}

View File

@@ -1,19 +1,5 @@
package com.commafeed.backend.favicon;
import java.io.IOException;
import java.time.Duration;
import java.util.Optional;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
@@ -21,170 +7,208 @@ import com.commafeed.backend.model.Feed;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
import java.time.Duration;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class YoutubeFaviconFetcherTest {
@Mock
private HttpGetter httpGetter;
@Mock private HttpGetter httpGetter;
@Mock
private CommaFeedConfiguration config;
@Mock private CommaFeedConfiguration config;
@Mock
private ObjectMapper objectMapper;
@Mock private ObjectMapper objectMapper;
private YoutubeFaviconFetcher faviconFetcher;
private YoutubeFaviconFetcher faviconFetcher;
@BeforeEach
void init() {
faviconFetcher = new YoutubeFaviconFetcher(httpGetter, config, objectMapper);
}
@BeforeEach
void init() {
faviconFetcher = new YoutubeFaviconFetcher(httpGetter, config, objectMapper);
}
@Test
void testFetchWithNonYoutubeUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
@Test
void testFetchWithNonYoutubeUrl() {
Feed feed = new Feed();
feed.setUrl("https://example.com/feed");
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter, objectMapper);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verifyNoInteractions(httpGetter, objectMapper);
}
@Test
void testFetchWithNoGoogleAuthKey() {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=someUser");
@Test
void testFetchWithNoGoogleAuthKey() {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=someUser");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.empty());
Mockito.when(config.googleAuthKey()).thenReturn(Optional.empty());
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verify(config).googleAuthKey();
Mockito.verifyNoInteractions(httpGetter, objectMapper);
}
Assertions.assertNull(faviconFetcher.fetch(feed));
Mockito.verify(config).googleAuthKey();
Mockito.verifyNoInteractions(httpGetter, objectMapper);
}
@Test
void testFetchForUser() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
@Test
void testFetchForUser() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
byte[] apiResponse = """
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes();
HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenReturn(apiHttpResult);
byte[] apiResponse =
"""
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
.getBytes();
HttpResult apiHttpResult =
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenReturn(apiHttpResult);
JsonNode jsonNode = new ObjectMapper().readTree(apiResponse);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
JsonNode jsonNode = new ObjectMapper().readTree(apiResponse);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchForChannel() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?channel_id=testChannelId");
@Test
void testFetchForChannel() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?channel_id=testChannelId");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
byte[] apiResponse = """
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes();
HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId"))
.thenReturn(apiHttpResult);
byte[] apiResponse =
"""
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
.getBytes();
HttpResult apiHttpResult =
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId"))
.thenReturn(apiHttpResult);
JsonNode jsonNode = new ObjectMapper().readTree(apiResponse);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
JsonNode jsonNode = new ObjectMapper().readTree(apiResponse);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchForPlaylist() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?playlist_id=testPlaylistId");
@Test
void testFetchForPlaylist() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?playlist_id=testPlaylistId");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
byte[] playlistResponse = """
{"items":[{"snippet":{"channelId":"testChannelId"}}]}""".getBytes();
HttpResult playlistHttpResult = new HttpResult(playlistResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/playlists?part=snippet&key=test-api-key&id=testPlaylistId"))
.thenReturn(playlistHttpResult);
byte[] playlistResponse =
"""
{"items":[{"snippet":{"channelId":"testChannelId"}}]}"""
.getBytes();
HttpResult playlistHttpResult =
new HttpResult(
playlistResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/playlists?part=snippet&key=test-api-key&id=testPlaylistId"))
.thenReturn(playlistHttpResult);
JsonNode playlistJsonNode = new ObjectMapper().readTree(playlistResponse);
Mockito.when(objectMapper.readTree(playlistResponse)).thenReturn(playlistJsonNode);
JsonNode playlistJsonNode = new ObjectMapper().readTree(playlistResponse);
Mockito.when(objectMapper.readTree(playlistResponse)).thenReturn(playlistJsonNode);
byte[] channelResponse = """
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}""".getBytes();
HttpResult channelHttpResult = new HttpResult(channelResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId"))
.thenReturn(channelHttpResult);
byte[] channelResponse =
"""
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
.getBytes();
HttpResult channelHttpResult =
new HttpResult(
channelResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&id=testChannelId"))
.thenReturn(channelHttpResult);
JsonNode channelJsonNode = new ObjectMapper().readTree(channelResponse);
Mockito.when(objectMapper.readTree(channelResponse)).thenReturn(channelJsonNode);
JsonNode channelJsonNode = new ObjectMapper().readTree(channelResponse);
Mockito.when(objectMapper.readTree(channelResponse)).thenReturn(channelJsonNode);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult = new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
byte[] iconBytes = new byte[1000];
String contentType = "image/png";
HttpResult iconHttpResult =
new HttpResult(iconBytes, contentType, null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://example.com/icon.png")).thenReturn(iconHttpResult);
Favicon result = faviconFetcher.fetch(feed);
Favicon result = faviconFetcher.fetch(feed);
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
Assertions.assertNotNull(result);
Assertions.assertEquals(iconBytes, result.icon());
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf(contentType)));
}
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
@Test
void testFetchWithHttpGetterException() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenThrow(new IOException("Network error"));
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenThrow(new IOException("Network error"));
Assertions.assertNull(faviconFetcher.fetch(feed));
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
@Test
void testFetchWithEmptyApiResponse() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
@Test
void testFetchWithEmptyApiResponse() throws Exception {
Feed feed = new Feed();
feed.setUrl("https://youtube.com/feeds/videos.xml?user=testUser");
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
Mockito.when(config.googleAuthKey()).thenReturn(Optional.of("test-api-key"));
byte[] apiResponse = "{}".getBytes();
HttpResult apiHttpResult = new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(httpGetter.get("https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenReturn(apiHttpResult);
byte[] apiResponse = "{}".getBytes();
HttpResult apiHttpResult =
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
Mockito.when(
httpGetter.get(
"https://www.googleapis.com/youtube/v3/channels?part=snippet&key=test-api-key&forUsername=testUser"))
.thenReturn(apiHttpResult);
JsonNode jsonNode = Mockito.mock(JsonNode.class);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
Mockito.when(jsonNode.at(Mockito.any(JsonPointer.class))).thenReturn(jsonNode);
Mockito.when(jsonNode.isMissingNode()).thenReturn(true);
JsonNode jsonNode = Mockito.mock(JsonNode.class);
Mockito.when(objectMapper.readTree(apiResponse)).thenReturn(jsonNode);
Mockito.when(jsonNode.at(Mockito.any(JsonPointer.class))).thenReturn(jsonNode);
Mockito.when(jsonNode.isMissingNode()).thenReturn(true);
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}
Assertions.assertNull(faviconFetcher.fetch(feed));
}
}

View File

@@ -1,17 +1,5 @@
package com.commafeed.backend.feed;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.Digests;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
@@ -21,63 +9,94 @@ import com.commafeed.backend.feed.parser.FeedParser;
import com.commafeed.backend.feed.parser.FeedParser.FeedParsingException;
import com.commafeed.backend.feed.parser.FeedParserResult;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class FeedFetcherTest {
@Mock
private FeedParser parser;
@Mock private FeedParser parser;
@Mock
private HttpGetter getter;
@Mock private HttpGetter getter;
@Mock
private FeedURLProvider urlProvider;
@Mock private FeedURLProvider urlProvider;
private FeedFetcher fetcher;
private FeedFetcher fetcher;
@BeforeEach
void init() {
fetcher = new FeedFetcher(parser, getter, List.of(urlProvider));
}
@BeforeEach
void init() {
fetcher = new FeedFetcher(parser, getter, List.of(urlProvider));
}
@Test
void findsUrlInPage() throws Exception {
String htmlUrl = "https://aaa.com";
byte[] html = "html".getBytes();
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(htmlUrl).build()))
.thenReturn(new HttpResult(html, "text/html", null, null, htmlUrl, Duration.ZERO));
Mockito.when(parser.parse(htmlUrl, html)).thenThrow(new FeedParsingException("invalid feed"));
@Test
void findsUrlInPage() throws Exception {
String htmlUrl = "https://aaa.com";
byte[] html = "html".getBytes();
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(htmlUrl).build()))
.thenReturn(new HttpResult(html, "text/html", null, null, htmlUrl, Duration.ZERO));
Mockito.when(parser.parse(htmlUrl, html))
.thenThrow(new FeedParsingException("invalid feed"));
String feedUrl = "https://bbb.com/feed";
byte[] feed = "feed".getBytes();
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(feedUrl).build()))
.thenReturn(new HttpResult(feed, "application/atom+xml", null, null, feedUrl, Duration.ZERO));
Mockito.when(parser.parse(feedUrl, feed)).thenReturn(new FeedParserResult("title", "link", "iconUrl", null, null, null, null));
String feedUrl = "https://bbb.com/feed";
byte[] feed = "feed".getBytes();
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(feedUrl).build()))
.thenReturn(
new HttpResult(
feed, "application/atom+xml", null, null, feedUrl, Duration.ZERO));
Mockito.when(parser.parse(feedUrl, feed))
.thenReturn(
new FeedParserResult("title", "link", "iconUrl", null, null, null, null));
Mockito.when(urlProvider.get(htmlUrl, new String(html))).thenReturn(List.of(feedUrl));
Mockito.when(urlProvider.get(htmlUrl, new String(html))).thenReturn(List.of(feedUrl));
FeedFetcherResult result = fetcher.fetch(htmlUrl, true, null, null, null, null);
Assertions.assertEquals("title", result.feed().title());
}
FeedFetcherResult result = fetcher.fetch(htmlUrl, true, null, null, null, null);
Assertions.assertEquals("title", result.feed().title());
}
@Test
void updatesHeaderWhenContentDitNotChange() throws Exception {
String url = "https://aaa.com";
String lastModified = "last-modified-1";
String etag = "etag-1";
byte[] content = "content".getBytes();
String lastContentHash = Digests.sha1Hex(content);
@Test
void updatesHeaderWhenContentDitNotChange() throws Exception {
String url = "https://aaa.com";
String lastModified = "last-modified-1";
String etag = "etag-1";
byte[] content = "content".getBytes();
String lastContentHash = Digests.sha1Hex(content);
Mockito.when(getter.get(HttpGetter.HttpRequest.builder(url).lastModified(lastModified).eTag(etag).build()))
.thenReturn(new HttpResult(content, "content-type", "last-modified-2", "etag-2", null, Duration.ZERO));
Mockito.when(
getter.get(
HttpGetter.HttpRequest.builder(url)
.lastModified(lastModified)
.eTag(etag)
.build()))
.thenReturn(
new HttpResult(
content,
"content-type",
"last-modified-2",
"etag-2",
null,
Duration.ZERO));
NotModifiedException e = Assertions.assertThrows(NotModifiedException.class,
() -> fetcher.fetch(url, false, lastModified, etag, Instant.now(), lastContentHash));
Assertions.assertEquals("last-modified-2", e.getNewLastModifiedHeader());
Assertions.assertEquals("etag-2", e.getNewEtagHeader());
}
NotModifiedException e =
Assertions.assertThrows(
NotModifiedException.class,
() ->
fetcher.fetch(
url,
false,
lastModified,
etag,
Instant.now(),
lastContentHash));
Assertions.assertEquals("last-modified-2", e.getNewLastModifiedHeader());
Assertions.assertEquals("etag-2", e.getNewEtagHeader());
}
}

View File

@@ -1,9 +1,10 @@
package com.commafeed.backend.feed;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -15,264 +16,289 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConfiguration.FeedRefreshErrorHandling;
@ExtendWith(MockitoExtension.class)
class FeedRefreshIntervalCalculatorTest {
private static final Instant NOW = Instant.now();
private static final Duration DEFAULT_INTERVAL = Duration.ofHours(1);
private static final Duration MAX_INTERVAL = Duration.ofDays(1);
private static final Instant NOW = Instant.now();
private static final Duration DEFAULT_INTERVAL = Duration.ofHours(1);
private static final Duration MAX_INTERVAL = Duration.ofDays(1);
@Mock
private InstantSource instantSource;
@Mock private InstantSource instantSource;
@Mock
private CommaFeedConfiguration config;
@Mock private CommaFeedConfiguration config;
@Mock
private FeedRefreshErrorHandling errorHandling;
@Mock private FeedRefreshErrorHandling errorHandling;
private FeedRefreshIntervalCalculator calculator;
private FeedRefreshIntervalCalculator calculator;
@BeforeEach
void setUp() {
Mockito.when(instantSource.instant()).thenReturn(NOW);
Mockito.when(config.feedRefresh()).thenReturn(Mockito.mock(CommaFeedConfiguration.FeedRefresh.class));
Mockito.when(config.feedRefresh().interval()).thenReturn(DEFAULT_INTERVAL);
Mockito.when(config.feedRefresh().maxInterval()).thenReturn(MAX_INTERVAL);
Mockito.when(config.feedRefresh().errors()).thenReturn(errorHandling);
@BeforeEach
void setUp() {
Mockito.when(instantSource.instant()).thenReturn(NOW);
Mockito.when(config.feedRefresh())
.thenReturn(Mockito.mock(CommaFeedConfiguration.FeedRefresh.class));
Mockito.when(config.feedRefresh().interval()).thenReturn(DEFAULT_INTERVAL);
Mockito.when(config.feedRefresh().maxInterval()).thenReturn(MAX_INTERVAL);
Mockito.when(config.feedRefresh().errors()).thenReturn(errorHandling);
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
@Nested
class FetchSuccess {
@Nested
class FetchSuccess {
@Nested
class EmpiricalDisabled {
@ParameterizedTest
@ValueSource(longs = { 0, 1, 300, 86400000L })
void withoutValidFor(long averageEntryInterval) {
// averageEntryInterval is ignored when empirical is disabled
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), averageEntryInterval, Duration.ZERO);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Nested
class EmpiricalDisabled {
@ParameterizedTest
@ValueSource(longs = {0, 1, 300, 86400000L})
void withoutValidFor(long averageEntryInterval) {
// averageEntryInterval is ignored when empirical is disabled
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), averageEntryInterval, Duration.ZERO);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void withValidForGreaterThanMaxInterval() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.plusDays(1));
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void withValidForGreaterThanMaxInterval() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.plusDays(1));
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void withValidForLowerThanMaxInterval() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.minusSeconds(1));
Assertions.assertEquals(NOW.plus(MAX_INTERVAL).minusSeconds(1), result);
}
}
@Test
void withValidForLowerThanMaxInterval() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), 1L, MAX_INTERVAL.minusSeconds(1));
Assertions.assertEquals(NOW.plus(MAX_INTERVAL).minusSeconds(1), result);
}
}
@Nested
class EmpiricalEnabled {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
@Nested
class EmpiricalEnabled {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
@Test
void withNullPublishedDate() {
Instant result = calculator.onFetchSuccess(null, 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void withNullPublishedDate() {
Instant result = calculator.onFetchSuccess(null, 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with31DaysOldPublishedDate() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(31)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with31DaysOldPublishedDate() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(31)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with15DaysOldPublishedDate() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(15)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
}
@Test
void with15DaysOldPublishedDate() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(15)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
}
@Test
void with8DaysOldPublishedDate() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(8)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void with8DaysOldPublishedDate() {
Instant result =
calculator.onFetchSuccess(NOW.minus(Duration.ofDays(8)), 1L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Nested
class FiveDaysOld {
@Test
void averageBetweenBounds() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis(),
Duration.ZERO);
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
}
@Nested
class FiveDaysOld {
@Test
void averageBetweenBounds() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)),
Duration.ofHours(4).toMillis(),
Duration.ZERO);
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
}
@Test
void averageBelowMinimum() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), 10L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void averageBelowMinimum() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), 10L, Duration.ZERO);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void averageAboveMaximum() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void averageAboveMaximum() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void noAverage() {
Instant result = calculator.onFetchSuccess(NOW.minus(Duration.ofDays(5)), null, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}
}
@Test
void noAverage() {
Instant result =
calculator.onFetchSuccess(
NOW.minus(Duration.ofDays(5)), null, Duration.ZERO);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}
}
@Nested
class FeedNotModified {
@Nested
class FeedNotModified {
@Nested
class EmpiricalDisabled {
@ParameterizedTest
@ValueSource(longs = { 0, 1, 300, 86400000L })
void withoutValidFor(long averageEntryInterval) {
// averageEntryInterval is ignored when empirical is disabled
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), averageEntryInterval);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
}
@Nested
class EmpiricalDisabled {
@ParameterizedTest
@ValueSource(longs = {0, 1, 300, 86400000L})
void withoutValidFor(long averageEntryInterval) {
// averageEntryInterval is ignored when empirical is disabled
Instant result =
calculator.onFeedNotModified(
NOW.minus(Duration.ofDays(5)), averageEntryInterval);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
}
@Nested
class EmpiricalEnabled {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
@Nested
class EmpiricalEnabled {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().intervalEmpirical()).thenReturn(true);
calculator = new FeedRefreshIntervalCalculator(config, instantSource);
}
@Test
void withNullPublishedDate() {
Instant result = calculator.onFeedNotModified(null, 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void withNullPublishedDate() {
Instant result = calculator.onFeedNotModified(null, 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with31DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(31)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with31DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(31)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
@Test
void with15DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(15)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
}
@Test
void with15DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(15)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(2)), result);
}
@Test
void with8DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(8)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void with8DaysOldPublishedDate() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(8)), 1L);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Nested
class FiveDaysOld {
@Test
void averageBetweenBounds() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis());
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
}
@Nested
class FiveDaysOld {
@Test
void averageBetweenBounds() {
Instant result =
calculator.onFeedNotModified(
NOW.minus(Duration.ofDays(5)), Duration.ofHours(4).toMillis());
Assertions.assertEquals(NOW.plus(Duration.ofHours(2)), result);
}
@Test
void averageBelowMinimum() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), 10L);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void averageBelowMinimum() {
Instant result =
calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), 10L);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void averageAboveMaximum() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void averageAboveMaximum() {
Instant result =
calculator.onFeedNotModified(
NOW.minus(Duration.ofDays(5)), Long.MAX_VALUE);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL.dividedBy(4)), result);
}
@Test
void noAverage() {
Instant result = calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), null);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}
}
@Test
void noAverage() {
Instant result =
calculator.onFeedNotModified(NOW.minus(Duration.ofDays(5)), null);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}
}
@Nested
class FetchError {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
}
@Nested
class FetchError {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
}
@Test
void lowErrorCount() {
Instant result = calculator.onFetchError(1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void lowErrorCount() {
Instant result = calculator.onFetchError(1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void highErrorCount() {
Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1));
@Test
void highErrorCount() {
Mockito.when(config.feedRefresh().errors().backoffInterval())
.thenReturn(Duration.ofHours(1));
Instant result = calculator.onFetchError(5);
Assertions.assertEquals(NOW.plus(Duration.ofHours(3)), result);
}
Instant result = calculator.onFetchError(5);
Assertions.assertEquals(NOW.plus(Duration.ofHours(3)), result);
}
@Test
void veryHighErrorCount() {
Mockito.when(config.feedRefresh().errors().backoffInterval()).thenReturn(Duration.ofHours(1));
@Test
void veryHighErrorCount() {
Mockito.when(config.feedRefresh().errors().backoffInterval())
.thenReturn(Duration.ofHours(1));
Instant result = calculator.onFetchError(100000);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
Instant result = calculator.onFetchError(100000);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
@Nested
class TooManyRequests {
@Nested
class TooManyRequests {
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
}
@BeforeEach
void setUp() {
Mockito.when(config.feedRefresh().errors().retriesBeforeBackoff()).thenReturn(3);
}
@Test
void withRetryAfterZero() {
Instant result = calculator.onTooManyRequests(NOW, 1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void withRetryAfterZero() {
Instant result = calculator.onTooManyRequests(NOW, 1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void withRetryAfterLowerThanInterval() {
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.minusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void withRetryAfterLowerThanInterval() {
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.minusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(NOW.plus(DEFAULT_INTERVAL), result);
}
@Test
void withRetryAfterBetweenBounds() {
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.plusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(retryAfter, result);
}
@Test
void withRetryAfterBetweenBounds() {
Instant retryAfter = NOW.plus(DEFAULT_INTERVAL.plusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(retryAfter, result);
}
@Test
void withRetryAfterGreaterThanMaxInterval() {
Instant retryAfter = NOW.plus(MAX_INTERVAL.plusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}
@Test
void withRetryAfterGreaterThanMaxInterval() {
Instant retryAfter = NOW.plus(MAX_INTERVAL.plusSeconds(10));
Instant result = calculator.onTooManyRequests(retryAfter, 1);
Assertions.assertEquals(NOW.plus(MAX_INTERVAL), result);
}
}
}

View File

@@ -1,41 +1,41 @@
package com.commafeed.backend.feed;
import java.time.Instant;
import java.util.Date;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.Entry;
import com.rometools.rome.feed.synd.SyndEntry;
import java.time.Instant;
import java.util.Date;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class FeedUtilsTest {
@Test
void asRss() {
Entry entry = new Entry();
entry.setId("1");
entry.setGuid("guid-1");
entry.setTitle("Test Entry");
entry.setContent("This is a test entry content.");
entry.setCategories("test,example");
entry.setRtl(false);
entry.setAuthor("Author Name");
entry.setEnclosureUrl("http://example.com/enclosure.mp3");
entry.setEnclosureType("audio/mpeg");
entry.setDate(Instant.ofEpochSecond(1));
entry.setUrl("http://example.com/test-entry");
@Test
void asRss() {
Entry entry = new Entry();
entry.setId("1");
entry.setGuid("guid-1");
entry.setTitle("Test Entry");
entry.setContent("This is a test entry content.");
entry.setCategories("test,example");
entry.setRtl(false);
entry.setAuthor("Author Name");
entry.setEnclosureUrl("http://example.com/enclosure.mp3");
entry.setEnclosureType("audio/mpeg");
entry.setDate(Instant.ofEpochSecond(1));
entry.setUrl("http://example.com/test-entry");
SyndEntry syndEntry = FeedUtils.asRss(entry);
Assertions.assertEquals("guid-1", syndEntry.getUri());
Assertions.assertEquals("Test Entry", syndEntry.getTitle());
Assertions.assertEquals("Author Name", syndEntry.getAuthor());
Assertions.assertEquals(1, syndEntry.getContents().size());
Assertions.assertEquals("This is a test entry content.", syndEntry.getContents().getFirst().getValue());
Assertions.assertEquals(1, syndEntry.getEnclosures().size());
Assertions.assertEquals("http://example.com/enclosure.mp3", syndEntry.getEnclosures().getFirst().getUrl());
Assertions.assertEquals("audio/mpeg", syndEntry.getEnclosures().getFirst().getType());
Assertions.assertEquals("http://example.com/test-entry", syndEntry.getLink());
Assertions.assertEquals(Date.from(Instant.ofEpochSecond(1)), syndEntry.getPublishedDate());
}
}
SyndEntry syndEntry = FeedUtils.asRss(entry);
Assertions.assertEquals("guid-1", syndEntry.getUri());
Assertions.assertEquals("Test Entry", syndEntry.getTitle());
Assertions.assertEquals("Author Name", syndEntry.getAuthor());
Assertions.assertEquals(1, syndEntry.getContents().size());
Assertions.assertEquals(
"This is a test entry content.", syndEntry.getContents().getFirst().getValue());
Assertions.assertEquals(1, syndEntry.getEnclosures().size());
Assertions.assertEquals(
"http://example.com/enclosure.mp3", syndEntry.getEnclosures().getFirst().getUrl());
Assertions.assertEquals("audio/mpeg", syndEntry.getEnclosures().getFirst().getType());
Assertions.assertEquals("http://example.com/test-entry", syndEntry.getLink());
Assertions.assertEquals(Date.from(Instant.ofEpochSecond(1)), syndEntry.getPublishedDate());
}
}

View File

@@ -8,56 +8,56 @@ import org.junit.jupiter.params.provider.ValueSource;
class ImageProxyUrlTest {
@BeforeAll
static void init() {
ImageProxyUrl.generateKey();
}
@BeforeAll
static void init() {
ImageProxyUrl.generateKey();
}
@ParameterizedTest
@ValueSource(
strings = {
// simple URL
"http://example.com/image.jpg",
@ParameterizedTest
@ValueSource(
strings = {
// simple URL
"http://example.com/image.jpg",
// URL with query parameters
"https://example.com/image.png?width=100&height=200",
// URL with query parameters
"https://example.com/image.png?width=100&height=200",
// URL with special characters
"https://example.com/path/image.gif?param=value&other=test#fragment",
// URL with special characters
"https://example.com/path/image.gif?param=value&other=test#fragment",
// URL with non-ascii characters
"https://example.com/image-ñáéíóú.jpg",
// URL with non-ascii characters
"https://example.com/image-ñáéíóú.jpg",
// blank URL
"",
// blank URL
"",
// URL with port number
"http://localhost:8080/images/photo.jpg",
// URL with port number
"http://localhost:8080/images/photo.jpg",
// long URL
"https://very-long-domain-name-example.com/very/long/path/to/image/file/with/many/segments/image.jpg?param1=value1&param2=value2&param3=value3",
// long URL
"https://very-long-domain-name-example.com/very/long/path/to/image/file/with/many/segments/image.jpg?param1=value1&param2=value2&param3=value3",
// URL with mixed case
"HTTPS://EXAMPLE.COM/Image.JPG",
// URL with mixed case
"HTTPS://EXAMPLE.COM/Image.JPG",
// URL with port number
"https://example123.com/image123.jpg?id=456789", })
void testEncodingDecoding(String originalUrl) {
String encoded = ImageProxyUrl.encode(originalUrl);
String decoded = ImageProxyUrl.decode(encoded);
// URL with port number
"https://example123.com/image123.jpg?id=456789",
})
void testEncodingDecoding(String originalUrl) {
String encoded = ImageProxyUrl.encode(originalUrl);
String decoded = ImageProxyUrl.decode(encoded);
Assertions.assertEquals(originalUrl, decoded);
}
Assertions.assertEquals(originalUrl, decoded);
}
@Test
void encodeProducesDifferentResultForDifferentUrls() {
String url1 = "https://example.com/image1.jpg";
String url2 = "https://example.com/image2.jpg";
@Test
void encodeProducesDifferentResultForDifferentUrls() {
String url1 = "https://example.com/image1.jpg";
String url2 = "https://example.com/image2.jpg";
String encoded1 = ImageProxyUrl.encode(url1);
String encoded2 = ImageProxyUrl.encode(url2);
String encoded1 = ImageProxyUrl.encode(url1);
String encoded2 = ImageProxyUrl.encode(url2);
Assertions.assertNotEquals(encoded1, encoded2);
}
}
Assertions.assertNotEquals(encoded1, encoded2);
}
}

View File

@@ -5,15 +5,20 @@ import org.junit.jupiter.api.Test;
class EncodingDetectorTest {
EncodingDetector encodingDetector = new EncodingDetector();
EncodingDetector encodingDetector = new EncodingDetector();
@Test
void testExtractDeclaredEncoding() {
Assertions.assertNull(encodingDetector.extractDeclaredEncoding("<?xml ?>".getBytes()));
Assertions.assertNull(encodingDetector.extractDeclaredEncoding("<feed></feed>".getBytes()));
Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("<?xml encoding=\"UTF-8\" ?>".getBytes()));
Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("<?xml encoding='UTF-8' ?>".getBytes()));
Assertions.assertEquals("UTF-8", encodingDetector.extractDeclaredEncoding("<?xml encoding='UTF-8'?>".getBytes()));
}
}
@Test
void testExtractDeclaredEncoding() {
Assertions.assertNull(encodingDetector.extractDeclaredEncoding("<?xml ?>".getBytes()));
Assertions.assertNull(encodingDetector.extractDeclaredEncoding("<feed></feed>".getBytes()));
Assertions.assertEquals(
"UTF-8",
encodingDetector.extractDeclaredEncoding("<?xml encoding=\"UTF-8\" ?>".getBytes()));
Assertions.assertEquals(
"UTF-8",
encodingDetector.extractDeclaredEncoding("<?xml encoding='UTF-8' ?>".getBytes()));
Assertions.assertEquals(
"UTF-8",
encodingDetector.extractDeclaredEncoding("<?xml encoding='UTF-8'?>".getBytes()));
}
}

View File

@@ -5,49 +5,93 @@ import org.junit.jupiter.api.Test;
class TextDirectionDetectorTest {
@Test
void testEstimateDirection() {
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(""));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(" "));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("! (...)"));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("Pure Ascii content"));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("-17.0%"));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect("http://foo/bar/"));
Assertions.assertEquals(TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect("http://foo/bar/?s=\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0"
+ "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0" + "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector.detect("\u05d0"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector.detect("\u05d0"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("http://foo/bar/ \u05d0 http://foo2/bar2/ http://foo3/bar3/"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("\u05d0\u05d9\u05df \u05de\u05de\u05e9 " + "\u05de\u05d4 \u05dc\u05e8\u05d0\u05d5\u05ea: "
+ "\u05dc\u05d0 \u05e6\u05d9\u05dc\u05de\u05ea\u05d9 " + "\u05d4\u05e8\u05d1\u05d4 \u05d5\u05d2\u05dd \u05d0"
+ "\u05dd \u05d4\u05d9\u05d9\u05ea\u05d9 \u05de\u05e6\u05dc" + "\u05dd, \u05d4\u05d9\u05d4 \u05e9\u05dd"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("\u05db\u05d0\u05df - http://geek.co.il/gallery/v/2007-06"
+ " - \u05d0\u05d9\u05df \u05de\u05de\u05e9 \u05de\u05d4 " + "\u05dc\u05e8\u05d0\u05d5\u05ea: \u05dc\u05d0 \u05e6"
+ "\u05d9\u05dc\u05de\u05ea\u05d9 \u05d4\u05e8\u05d1\u05d4 "
+ "\u05d5\u05d2\u05dd \u05d0\u05dd \u05d4\u05d9\u05d9\u05ea"
+ "\u05d9 \u05de\u05e6\u05dc\u05dd, \u05d4\u05d9\u05d4 "
+ "\u05e9\u05dd \u05d1\u05e2\u05d9\u05e7\u05e8 \u05d4\u05e8" + "\u05d1\u05d4 \u05d0\u05e0\u05e9\u05d9\u05dd. \u05de"
+ "\u05d4 \u05e9\u05db\u05df - \u05d0\u05e4\u05e9\u05e8 " + "\u05dc\u05e0\u05e6\u05dc \u05d0\u05ea \u05d4\u05d4 "
+ "\u05d3\u05d6\u05de\u05e0\u05d5\u05ea \u05dc\u05d4\u05e1" + "\u05ea\u05db\u05dc \u05e2\u05dc \u05db\u05de\u05d4 "
+ "\u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05de\u05e9\u05e2"
+ "\u05e9\u05e2\u05d5\u05ea \u05d9\u05e9\u05e0\u05d5\u05ea " + "\u05d9\u05d5\u05ea\u05e8 \u05e9\u05d9\u05e9 \u05dc"
+ "\u05d9 \u05d1\u05d0\u05ea\u05e8"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("CAPTCHA \u05de\u05e9\u05d5\u05db\u05dc\u05dc " + "\u05de\u05d3\u05d9?"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("Yes Prime Minister \u05e2\u05d3\u05db\u05d5\u05df. "
+ "\u05e9\u05d0\u05dc\u05d5 \u05d0\u05d5\u05ea\u05d9 " + "\u05de\u05d4 \u05d0\u05e0\u05d9 \u05e8\u05d5\u05e6"
+ "\u05d4 \u05de\u05ea\u05e0\u05d4 \u05dc\u05d7\u05d2"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT, TextDirectionDetector
.detect("17.4.02 \u05e9\u05e2\u05d4:13-20 .15-00 .\u05dc\u05d0 " + "\u05d4\u05d9\u05d9\u05ea\u05d9 \u05db\u05d0\u05df."));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("5710 5720 5730. \u05d4\u05d3\u05dc\u05ea. " + "\u05d4\u05e0\u05e9\u05d9\u05e7\u05d4"));
Assertions.assertEquals(TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("\u05d4\u05d3\u05dc\u05ea http://www.google.com " + "http://www.gmail.com"));
}
}
@Test
void testEstimateDirection() {
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(""));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT, TextDirectionDetector.detect(" "));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect("! (...)"));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect("Pure Ascii content"));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect("-17.0%"));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect("http://foo/bar/"));
Assertions.assertEquals(
TextDirectionDetector.Direction.LEFT_TO_RIGHT,
TextDirectionDetector.detect(
"http://foo/bar/?s=\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0"
+ "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0"
+ "\u05d0\u05d0\u05d0\u05d0\u05d0\u05d0"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("\u05d0"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect("\u05d0"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"http://foo/bar/ \u05d0 http://foo2/bar2/ http://foo3/bar3/"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"\u05d0\u05d9\u05df \u05de\u05de\u05e9 "
+ "\u05de\u05d4 \u05dc\u05e8\u05d0\u05d5\u05ea: "
+ "\u05dc\u05d0 \u05e6\u05d9\u05dc\u05de\u05ea\u05d9 "
+ "\u05d4\u05e8\u05d1\u05d4 \u05d5\u05d2\u05dd \u05d0"
+ "\u05dd \u05d4\u05d9\u05d9\u05ea\u05d9 \u05de\u05e6\u05dc"
+ "\u05dd, \u05d4\u05d9\u05d4 \u05e9\u05dd"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"\u05db\u05d0\u05df - http://geek.co.il/gallery/v/2007-06"
+ " - \u05d0\u05d9\u05df \u05de\u05de\u05e9 \u05de\u05d4 "
+ "\u05dc\u05e8\u05d0\u05d5\u05ea: \u05dc\u05d0 \u05e6"
+ "\u05d9\u05dc\u05de\u05ea\u05d9 \u05d4\u05e8\u05d1\u05d4 "
+ "\u05d5\u05d2\u05dd \u05d0\u05dd \u05d4\u05d9\u05d9\u05ea"
+ "\u05d9 \u05de\u05e6\u05dc\u05dd, \u05d4\u05d9\u05d4 "
+ "\u05e9\u05dd \u05d1\u05e2\u05d9\u05e7\u05e8 \u05d4\u05e8"
+ "\u05d1\u05d4 \u05d0\u05e0\u05e9\u05d9\u05dd. \u05de"
+ "\u05d4 \u05e9\u05db\u05df - \u05d0\u05e4\u05e9\u05e8 "
+ "\u05dc\u05e0\u05e6\u05dc \u05d0\u05ea \u05d4\u05d4 "
+ "\u05d3\u05d6\u05de\u05e0\u05d5\u05ea \u05dc\u05d4\u05e1"
+ "\u05ea\u05db\u05dc \u05e2\u05dc \u05db\u05de\u05d4 "
+ "\u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05de\u05e9\u05e2"
+ "\u05e9\u05e2\u05d5\u05ea \u05d9\u05e9\u05e0\u05d5\u05ea "
+ "\u05d9\u05d5\u05ea\u05e8 \u05e9\u05d9\u05e9 \u05dc"
+ "\u05d9 \u05d1\u05d0\u05ea\u05e8"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"CAPTCHA \u05de\u05e9\u05d5\u05db\u05dc\u05dc " + "\u05de\u05d3\u05d9?"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"Yes Prime Minister \u05e2\u05d3\u05db\u05d5\u05df. "
+ "\u05e9\u05d0\u05dc\u05d5 \u05d0\u05d5\u05ea\u05d9 "
+ "\u05de\u05d4 \u05d0\u05e0\u05d9 \u05e8\u05d5\u05e6"
+ "\u05d4 \u05de\u05ea\u05e0\u05d4 \u05dc\u05d7\u05d2"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"17.4.02 \u05e9\u05e2\u05d4:13-20 .15-00 .\u05dc\u05d0 "
+ "\u05d4\u05d9\u05d9\u05ea\u05d9 \u05db\u05d0\u05df."));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"5710 5720 5730. \u05d4\u05d3\u05dc\u05ea. "
+ "\u05d4\u05e0\u05e9\u05d9\u05e7\u05d4"));
Assertions.assertEquals(
TextDirectionDetector.Direction.RIGHT_TO_LEFT,
TextDirectionDetector.detect(
"\u05d4\u05d3\u05dc\u05ea http://www.google.com "
+ "http://www.gmail.com"));
}
}

View File

@@ -6,266 +6,297 @@ import org.junit.jupiter.api.Test;
class XMLCleanerTest {
XMLCleaner xmlCleaner = new XMLCleaner();
XMLCleaner xmlCleaner = new XMLCleaner();
@Nested
class RemoveCharactersBeforeFirstXmlTag {
@Test
void removesWhitespaceBeforeXmlTag() {
String xml = " \n\t<feed>content</feed>";
Assertions.assertEquals("<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Nested
class RemoveCharactersBeforeFirstXmlTag {
@Test
void removesWhitespaceBeforeXmlTag() {
String xml = " \n\t<feed>content</feed>";
Assertions.assertEquals(
"<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void removesTextBeforeXmlTag() {
String xml = "some text here<feed>content</feed>";
Assertions.assertEquals("<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void removesTextBeforeXmlTag() {
String xml = "some text here<feed>content</feed>";
Assertions.assertEquals(
"<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void returnsUnchangedWhenStartsWithXmlTag() {
String xml = "<feed>content</feed>";
Assertions.assertEquals("<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void returnsUnchangedWhenStartsWithXmlTag() {
String xml = "<feed>content</feed>";
Assertions.assertEquals(
"<feed>content</feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void returnsNullWhenNoXmlTagFound() {
String xml = "no xml tags here";
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void returnsNullWhenNoXmlTagFound() {
String xml = "no xml tags here";
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(null));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(null));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(""));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(""));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(" \n\t "));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeCharactersBeforeFirstXmlTag(" \n\t "));
}
@Test
void preservesMultipleXmlTags() {
String xml = "garbage<feed><item>content</item></feed>";
Assertions.assertEquals("<feed><item>content</item></feed>", xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
}
@Test
void preservesMultipleXmlTags() {
String xml = "garbage<feed><item>content</item></feed>";
Assertions.assertEquals(
"<feed><item>content</item></feed>",
xmlCleaner.removeCharactersBeforeFirstXmlTag(xml));
}
}
@Nested
class RemoveInvalidXmlCharacters {
@Test
void removesNullCharacter() {
String xml = "<feed>content\u0000here</feed>";
Assertions.assertEquals("<feed>contenthere</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Nested
class RemoveInvalidXmlCharacters {
@Test
void removesNullCharacter() {
String xml = "<feed>content\u0000here</feed>";
Assertions.assertEquals(
"<feed>contenthere</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void removesInvalidControlCharacters() {
String xml = "<feed>content\u0001\u0002\u0003here</feed>";
Assertions.assertEquals("<feed>contenthere</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void removesInvalidControlCharacters() {
String xml = "<feed>content\u0001\u0002\u0003here</feed>";
Assertions.assertEquals(
"<feed>contenthere</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesValidXmlCharacters() {
String xml = "<feed>content with\ttab\nand newline</feed>";
Assertions.assertEquals("<feed>content with\ttab\nand newline</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesValidXmlCharacters() {
String xml = "<feed>content with\ttab\nand newline</feed>";
Assertions.assertEquals(
"<feed>content with\ttab\nand newline</feed>",
xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesUnicodeCharacters() {
String xml = "<feed>café résumé 中文 العربية</feed>";
Assertions.assertEquals("<feed>café résumé 中文 العربية</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesUnicodeCharacters() {
String xml = "<feed>café résumé 中文 العربية</feed>";
Assertions.assertEquals(
"<feed>café résumé 中文 العربية</feed>",
xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesEmojiCharacters() {
String xml = "<feed>🎮💪✅</feed>";
Assertions.assertEquals("<feed>🎮💪✅</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void preservesEmojiCharacters() {
String xml = "<feed>🎮💪✅</feed>";
Assertions.assertEquals(
"<feed>🎮💪✅</feed>", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void removesMultipleInvalidCharacters() {
String xml = "test\u0000test\u0001test\u0002test";
Assertions.assertEquals("testtesttesttest", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void removesMultipleInvalidCharacters() {
String xml = "test\u0000test\u0001test\u0002test";
Assertions.assertEquals("testtesttesttest", xmlCleaner.removeInvalidXmlCharacters(xml));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(null));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(null));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(""));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(""));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(" "));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeInvalidXmlCharacters(" "));
}
@Test
void handlesStringWithOnlyInvalidCharacters() {
String xml = "\u0000\u0001\u0002";
Assertions.assertEquals("", xmlCleaner.removeInvalidXmlCharacters(xml));
}
}
@Test
void handlesStringWithOnlyInvalidCharacters() {
String xml = "\u0000\u0001\u0002";
Assertions.assertEquals("", xmlCleaner.removeInvalidXmlCharacters(xml));
}
}
@Nested
class Entities {
@Test
void testReplaceHtmlEntitiesWithNumericEntities() {
String source = "<source>T&acute;l&acute;phone &prime;</source>";
Assertions.assertEquals("<source>T&#180;l&#180;phone &#8242;</source>",
xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Nested
class Entities {
@Test
void testReplaceHtmlEntitiesWithNumericEntities() {
String source = "<source>T&acute;l&acute;phone &prime;</source>";
Assertions.assertEquals(
"<source>T&#180;l&#180;phone &#8242;</source>",
xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void replacesMultipleOccurrencesOfSameEntity() {
String source = "&nbsp;&nbsp;&nbsp;";
Assertions.assertEquals("&#160;&#160;&#160;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void replacesMultipleOccurrencesOfSameEntity() {
String source = "&nbsp;&nbsp;&nbsp;";
Assertions.assertEquals(
"&#160;&#160;&#160;",
xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void preservesTextWithoutEntities() {
String source = "<feed>regular content</feed>";
Assertions.assertEquals("<feed>regular content</feed>", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void preservesTextWithoutEntities() {
String source = "<feed>regular content</feed>";
Assertions.assertEquals(
"<feed>regular content</feed>",
xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void preservesNumericEntities() {
String source = "&#180;&#8242;";
Assertions.assertEquals("&#180;&#8242;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void preservesNumericEntities() {
String source = "&#180;&#8242;";
Assertions.assertEquals(
"&#180;&#8242;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void replacesCommonHtmlEntities() {
String source = "&amp;&quot;";
Assertions.assertEquals("&#38;&#34;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void replacesCommonHtmlEntities() {
String source = "&amp;&quot;";
Assertions.assertEquals(
"&#38;&#34;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void handlesPartialEntityMatches() {
String source = "&amplifier";
String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source);
Assertions.assertTrue(result.startsWith("&#38;") || result.equals("&amplifier"));
}
@Test
void handlesPartialEntityMatches() {
String source = "&amplifier";
String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source);
Assertions.assertTrue(result.startsWith("&#38;") || result.equals("&amplifier"));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(null));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(null));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(""));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(""));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(" "));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.replaceHtmlEntitiesWithNumericEntities(" "));
}
@Test
void handlesEntityAtStartOfString() {
String source = "&amp;test";
Assertions.assertEquals("&#38;test", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void handlesEntityAtStartOfString() {
String source = "&amp;test";
Assertions.assertEquals(
"&#38;test", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void handlesEntityAtEndOfString() {
String source = "test&amp;";
Assertions.assertEquals("test&#38;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void handlesEntityAtEndOfString() {
String source = "test&amp;";
Assertions.assertEquals(
"test&#38;", xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source));
}
@Test
void handlesMixedEntitiesAndText() {
String source = "Hello&nbsp;World&excl;&nbsp;Test&period;";
String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source);
Assertions.assertTrue(result.contains("&#"));
}
}
@Test
void handlesMixedEntitiesAndText() {
String source = "Hello&nbsp;World&excl;&nbsp;Test&period;";
String result = xmlCleaner.replaceHtmlEntitiesWithNumericEntities(source);
Assertions.assertTrue(result.contains("&#"));
}
}
@Nested
class Doctype {
@Test
void testRemoveDoctype() {
String source = "<!DOCTYPE html><html><head></head><body></body></html>";
Assertions.assertEquals("<html><head></head><body></body></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Nested
class Doctype {
@Test
void testRemoveDoctype() {
String source = "<!DOCTYPE html><html><head></head><body></body></html>";
Assertions.assertEquals(
"<html><head></head><body></body></html>",
xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void testRemoveMultilineDoctype() {
String source = """
@Test
void testRemoveMultilineDoctype() {
String source =
"""
<!DOCTYPE
html
>
<html><head></head><body></body></html>""";
Assertions.assertEquals("""
Assertions.assertEquals(
"""
<html><head></head><body></body></html>""", xmlCleaner.removeDoctypeDeclarations(source));
}
<html><head></head><body></body></html>""",
xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesComplexDoctypeWithSystemId() {
String source = "<!DOCTYPE html SYSTEM \"about:legacy-compat\"><html><body></body></html>";
Assertions.assertEquals("<html><body></body></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesComplexDoctypeWithSystemId() {
String source =
"<!DOCTYPE html SYSTEM \"about:legacy-compat\"><html><body></body></html>";
Assertions.assertEquals(
"<html><body></body></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesComplexDoctypeWithPublicId() {
String source = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesComplexDoctypeWithPublicId() {
String source =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesCaseInsensitiveDoctype() {
String source = "<!doctype html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesCaseInsensitiveDoctype() {
String source = "<!doctype html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesMixedCaseDoctype() {
String source = "<!DoCtYpE html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesMixedCaseDoctype() {
String source = "<!DoCtYpE html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesMultipleDoctypeDeclarations() {
String source = "<!DOCTYPE html><!DOCTYPE html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void removesMultipleDoctypeDeclarations() {
String source = "<!DOCTYPE html><!DOCTYPE html><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void preservesContentWithoutDoctype() {
String source = "<html><body>No doctype here</body></html>";
Assertions.assertEquals("<html><body>No doctype here</body></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void preservesContentWithoutDoctype() {
String source = "<html><body>No doctype here</body></html>";
Assertions.assertEquals(
"<html><body>No doctype here</body></html>",
xmlCleaner.removeDoctypeDeclarations(source));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(null));
}
@Test
void returnsNullWhenInputIsNull() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(null));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(""));
}
@Test
void returnsNullWhenInputIsEmpty() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(""));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(" "));
}
@Test
void returnsNullWhenInputIsBlank() {
Assertions.assertNull(xmlCleaner.removeDoctypeDeclarations(" "));
}
@Test
void handlesDoctypeWithExtraWhitespace() {
String source = "<!DOCTYPE html ><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
}
@Test
void handlesDoctypeWithExtraWhitespace() {
String source = "<!DOCTYPE html ><html></html>";
Assertions.assertEquals("<html></html>", xmlCleaner.removeDoctypeDeclarations(source));
}
}
}

View File

@@ -6,135 +6,363 @@ import org.junit.jupiter.api.Test;
class FeedEntryContentTest {
@Nested
class EquivalentTo {
@Nested
class EquivalentTo {
@Test
void shouldReturnFalseWhenComparedWithNull() {
Assertions.assertFalse(new FeedEntryContent().equivalentTo(null));
}
@Test
void shouldReturnFalseWhenComparedWithNull() {
Assertions.assertFalse(new FeedEntryContent().equivalentTo(null));
}
@Test
void shouldReturnTrueWhenComparedWithIdenticalContent() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertTrue(content1.equivalentTo(content2));
}
@Test
void shouldReturnTrueWhenComparedWithIdenticalContent() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertTrue(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenTitleDiffers() {
FeedEntryContent content1 = createContent("title1", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title2", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenTitleDiffers() {
FeedEntryContent content1 =
createContent(
"title1",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title2",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenContentDiffers() {
FeedEntryContent content1 = createContent("title", "content1", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content2", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenContentDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content1",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content2",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenAuthorDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author1", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author2", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenAuthorDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author1",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author2",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenCategoriesDiffer() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories1", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories2", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenCategoriesDiffer() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories1",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories2",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenEnclosureUrlDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl1", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl2", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenEnclosureUrlDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl1",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl2",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenEnclosureTypeDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType1",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType2",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenEnclosureTypeDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType1",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType2",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaDescriptionDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription1", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription2", "mediaThumbnailUrl", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaDescriptionDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription1",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription2",
"mediaThumbnailUrl",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailUrlDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl1", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl2", 10, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailUrlDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl1",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl2",
10,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailWidthDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 15, 20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailWidthDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
15,
20);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailHeightDiffers() {
FeedEntryContent content1 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 20);
FeedEntryContent content2 = createContent("title", "content", "author", "categories", "enclosureUrl", "enclosureType",
"mediaDescription", "mediaThumbnailUrl", 10, 25);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnFalseWhenMediaThumbnailHeightDiffers() {
FeedEntryContent content1 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
20);
FeedEntryContent content2 =
createContent(
"title",
"content",
"author",
"categories",
"enclosureUrl",
"enclosureType",
"mediaDescription",
"mediaThumbnailUrl",
10,
25);
Assertions.assertFalse(content1.equivalentTo(content2));
}
@Test
void shouldReturnTrueWhenNullFieldsAreEqual() {
FeedEntryContent content1 = new FeedEntryContent();
FeedEntryContent content2 = new FeedEntryContent();
Assertions.assertTrue(content1.equivalentTo(content2));
}
@Test
void shouldReturnTrueWhenNullFieldsAreEqual() {
FeedEntryContent content1 = new FeedEntryContent();
FeedEntryContent content2 = new FeedEntryContent();
Assertions.assertTrue(content1.equivalentTo(content2));
}
private FeedEntryContent createContent(String title, String content, String author, String categories, String enclosureUrl,
String enclosureType, String mediaDescription, String mediaThumbnailUrl, Integer mediaThumbnailWidth,
Integer mediaThumbnailHeight) {
FeedEntryContent feedEntryContent = new FeedEntryContent();
feedEntryContent.setTitle(title);
feedEntryContent.setContent(content);
feedEntryContent.setAuthor(author);
feedEntryContent.setCategories(categories);
feedEntryContent.setEnclosureUrl(enclosureUrl);
feedEntryContent.setEnclosureType(enclosureType);
feedEntryContent.setMediaDescription(mediaDescription);
feedEntryContent.setMediaThumbnailUrl(mediaThumbnailUrl);
feedEntryContent.setMediaThumbnailWidth(mediaThumbnailWidth);
feedEntryContent.setMediaThumbnailHeight(mediaThumbnailHeight);
return feedEntryContent;
}
}
}
private FeedEntryContent createContent(
String title,
String content,
String author,
String categories,
String enclosureUrl,
String enclosureType,
String mediaDescription,
String mediaThumbnailUrl,
Integer mediaThumbnailWidth,
Integer mediaThumbnailHeight) {
FeedEntryContent feedEntryContent = new FeedEntryContent();
feedEntryContent.setTitle(title);
feedEntryContent.setContent(content);
feedEntryContent.setAuthor(author);
feedEntryContent.setCategories(categories);
feedEntryContent.setEnclosureUrl(enclosureUrl);
feedEntryContent.setEnclosureType(enclosureType);
feedEntryContent.setMediaDescription(mediaDescription);
feedEntryContent.setMediaThumbnailUrl(mediaThumbnailUrl);
feedEntryContent.setMediaThumbnailWidth(mediaThumbnailWidth);
feedEntryContent.setMediaThumbnailHeight(mediaThumbnailHeight);
return feedEntryContent;
}
}
}

View File

@@ -1,16 +1,5 @@
package com.commafeed.backend.opml;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.Feed;
@@ -19,115 +8,124 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OPMLExporterTest {
@Mock
private FeedCategoryDAO feedCategoryDAO;
@Mock
private FeedSubscriptionDAO feedSubscriptionDAO;
@Mock private FeedCategoryDAO feedCategoryDAO;
@Mock private FeedSubscriptionDAO feedSubscriptionDAO;
private final User user = new User();
private final User user = new User();
private final FeedCategory cat1 = new FeedCategory();
private final FeedCategory cat2 = new FeedCategory();
private final FeedCategory cat1 = new FeedCategory();
private final FeedCategory cat2 = new FeedCategory();
private final FeedSubscription rootFeed = newFeedSubscription("rootFeed", "rootFeed.com");
private final FeedSubscription cat1Feed = newFeedSubscription("cat1Feed", "cat1Feed.com");
private final FeedSubscription cat2Feed = newFeedSubscription("cat2Feed", "cat2Feed.com");
private final FeedSubscription rootFeed = newFeedSubscription("rootFeed", "rootFeed.com");
private final FeedSubscription cat1Feed = newFeedSubscription("cat1Feed", "cat1Feed.com");
private final FeedSubscription cat2Feed = newFeedSubscription("cat2Feed", "cat2Feed.com");
private final List<FeedCategory> categories = new ArrayList<>();
private final List<FeedSubscription> subscriptions = new ArrayList<>();
private final List<FeedCategory> categories = new ArrayList<>();
private final List<FeedSubscription> subscriptions = new ArrayList<>();
@BeforeEach
void init() {
user.setName("John Doe");
@BeforeEach
void init() {
user.setName("John Doe");
cat1.setId(1L);
cat1.setName("cat1");
cat1.setParent(null);
cat1.setId(1L);
cat1.setName("cat1");
cat1.setParent(null);
cat2.setId(2L);
cat2.setName("cat2");
cat2.setParent(cat1);
cat2.setId(2L);
cat2.setName("cat2");
cat2.setParent(cat1);
rootFeed.setCategory(null);
cat1Feed.setCategory(cat1);
cat2Feed.setCategory(cat2);
rootFeed.setCategory(null);
cat1Feed.setCategory(cat1);
cat2Feed.setCategory(cat2);
categories.add(cat1);
categories.add(cat2);
categories.add(cat1);
categories.add(cat2);
subscriptions.add(rootFeed);
subscriptions.add(cat1Feed);
subscriptions.add(cat2Feed);
}
subscriptions.add(rootFeed);
subscriptions.add(cat1Feed);
subscriptions.add(cat2Feed);
}
private Feed newFeed(String url) {
Feed feed = new Feed();
feed.setUrl(url);
return feed;
}
private Feed newFeed(String url) {
Feed feed = new Feed();
feed.setUrl(url);
return feed;
}
private FeedSubscription newFeedSubscription(String title, String url) {
FeedSubscription feedSubscription = new FeedSubscription();
feedSubscription.setTitle(title);
feedSubscription.setFeed(newFeed(url));
return feedSubscription;
}
private FeedSubscription newFeedSubscription(String title, String url) {
FeedSubscription feedSubscription = new FeedSubscription();
feedSubscription.setTitle(title);
feedSubscription.setFeed(newFeed(url));
return feedSubscription;
}
@Test
void generatesOpmlCorrectly() {
Mockito.when(feedCategoryDAO.findAll(user)).thenReturn(categories);
Mockito.when(feedSubscriptionDAO.findAll(user)).thenReturn(subscriptions);
@Test
void generatesOpmlCorrectly() {
Mockito.when(feedCategoryDAO.findAll(user)).thenReturn(categories);
Mockito.when(feedSubscriptionDAO.findAll(user)).thenReturn(subscriptions);
Opml opml = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO).export(user);
Opml opml = new OPMLExporter(feedCategoryDAO, feedSubscriptionDAO).export(user);
List<Outline> rootOutlines = opml.getOutlines();
Assertions.assertEquals(2, rootOutlines.size());
Assertions.assertTrue(containsCategory(rootOutlines, "cat1"));
Assertions.assertTrue(containsFeed(rootOutlines, "rootFeed", "rootFeed.com"));
List<Outline> rootOutlines = opml.getOutlines();
Assertions.assertEquals(2, rootOutlines.size());
Assertions.assertTrue(containsCategory(rootOutlines, "cat1"));
Assertions.assertTrue(containsFeed(rootOutlines, "rootFeed", "rootFeed.com"));
Outline cat1Outline = getCategoryOutline(rootOutlines, "cat1");
List<Outline> cat1Children = cat1Outline.getChildren();
Assertions.assertEquals(2, cat1Children.size());
Assertions.assertTrue(containsCategory(cat1Children, "cat2"));
Assertions.assertTrue(containsFeed(cat1Children, "cat1Feed", "cat1Feed.com"));
Outline cat1Outline = getCategoryOutline(rootOutlines, "cat1");
List<Outline> cat1Children = cat1Outline.getChildren();
Assertions.assertEquals(2, cat1Children.size());
Assertions.assertTrue(containsCategory(cat1Children, "cat2"));
Assertions.assertTrue(containsFeed(cat1Children, "cat1Feed", "cat1Feed.com"));
Outline cat2Outline = getCategoryOutline(cat1Children, "cat2");
List<Outline> cat2Children = cat2Outline.getChildren();
Assertions.assertEquals(1, cat2Children.size());
Assertions.assertTrue(containsFeed(cat2Children, "cat2Feed", "cat2Feed.com"));
}
Outline cat2Outline = getCategoryOutline(cat1Children, "cat2");
List<Outline> cat2Children = cat2Outline.getChildren();
Assertions.assertEquals(1, cat2Children.size());
Assertions.assertTrue(containsFeed(cat2Children, "cat2Feed", "cat2Feed.com"));
}
private boolean containsCategory(List<Outline> outlines, String category) {
for (Outline o : outlines) {
if (!"rss".equals(o.getType()) && category.equals(o.getTitle())) {
return true;
}
}
private boolean containsCategory(List<Outline> outlines, String category) {
for (Outline o : outlines) {
if (!"rss".equals(o.getType()) && category.equals(o.getTitle())) {
return true;
}
}
return false;
}
return false;
}
private boolean containsFeed(List<Outline> outlines, String title, String url) {
for (Outline o : outlines) {
if ("rss".equals(o.getType()) && title.equals(o.getTitle()) && o.getAttributeValue("xmlUrl").equals(url)) {
return true;
}
}
private boolean containsFeed(List<Outline> outlines, String title, String url) {
for (Outline o : outlines) {
if ("rss".equals(o.getType())
&& title.equals(o.getTitle())
&& o.getAttributeValue("xmlUrl").equals(url)) {
return true;
}
}
return false;
}
return false;
}
private Outline getCategoryOutline(List<Outline> outlines, String title) {
for (Outline o : outlines) {
if (o.getTitle().equals(title)) {
return o;
}
}
private Outline getCategoryOutline(List<Outline> outlines, String title) {
for (Outline o : outlines) {
if (o.getTitle().equals(title)) {
return o;
}
}
return null;
}
}
return null;
}
}

View File

@@ -1,55 +1,62 @@
package com.commafeed.backend.opml;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.feed.parser.XMLCleaner;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.FeedSubscriptionService;
import com.rometools.rome.io.FeedException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class OPMLImporterTest {
@Test
void testOpmlV10() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v1.0.xml");
}
@Test
void testOpmlV10() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v1.0.xml");
}
@Test
void testOpmlV11() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v1.1.xml");
}
@Test
void testOpmlV11() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v1.1.xml");
}
@Test
void testOpmlV20() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v2.0.xml");
}
@Test
void testOpmlV20() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_v2.0.xml");
}
@Test
void testOpmlNoVersion() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_noversion.xml");
}
@Test
void testOpmlNoVersion() throws IOException, IllegalArgumentException, FeedException {
testOpmlVersion("/opml/opml_noversion.xml");
}
private void testOpmlVersion(String fileName) throws IOException, IllegalArgumentException, FeedException {
XMLCleaner xmlCleaner = Mockito.mock(XMLCleaner.class);
FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class);
FeedSubscriptionService feedSubscriptionService = Mockito.mock(FeedSubscriptionService.class);
User user = Mockito.mock(User.class);
private void testOpmlVersion(String fileName)
throws IOException, IllegalArgumentException, FeedException {
XMLCleaner xmlCleaner = Mockito.mock(XMLCleaner.class);
FeedCategoryDAO feedCategoryDAO = Mockito.mock(FeedCategoryDAO.class);
FeedSubscriptionService feedSubscriptionService =
Mockito.mock(FeedSubscriptionService.class);
User user = Mockito.mock(User.class);
Mockito.when(xmlCleaner.clean(Mockito.anyString())).thenAnswer(invocation -> invocation.getArgument(0));
String xml = IOUtils.toString(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8);
Mockito.when(xmlCleaner.clean(Mockito.anyString()))
.thenAnswer(invocation -> invocation.getArgument(0));
String xml =
IOUtils.toString(getClass().getResourceAsStream(fileName), StandardCharsets.UTF_8);
OPMLImporter importer = new OPMLImporter(xmlCleaner, feedCategoryDAO, feedSubscriptionService);
importer.importOpml(user, xml);
Mockito.verify(feedSubscriptionService)
.subscribe(Mockito.eq(user), Mockito.anyString(), Mockito.anyString(), Mockito.any(FeedCategory.class), Mockito.anyInt());
}
OPMLImporter importer =
new OPMLImporter(xmlCleaner, feedCategoryDAO, feedSubscriptionService);
importer.importOpml(user, xml);
Mockito.verify(feedSubscriptionService)
.subscribe(
Mockito.eq(user),
Mockito.anyString(),
Mockito.anyString(),
Mockito.any(FeedCategory.class),
Mockito.anyInt());
}
}

View File

@@ -5,27 +5,31 @@ import org.junit.jupiter.api.Test;
class FeedEntryContentCleaningServiceTest {
private final FeedEntryContentCleaningService feedEntryContentCleaningService = new FeedEntryContentCleaningService();
private final FeedEntryContentCleaningService feedEntryContentCleaningService =
new FeedEntryContentCleaningService();
@Test
void testClean() {
String content = """
@Test
void testClean() {
String content =
"""
<p>
Some text
<img width="965" height="320" src="https://localhost/an-image.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="alt-desc" decoding="async" sizes="(max-width: 965px) 100vw, 965px" style="width: 100%; opacity: 0">
<iframe src="url" style="width: 100%; opacity: 0"></iframe>
<forbidden-element>aaa</forbidden-element>
""";
String result = feedEntryContentCleaningService.clean(content, "baseUri", false);
String result = feedEntryContentCleaningService.clean(content, "baseUri", false);
Assertions.assertLinesMatch("""
Assertions.assertLinesMatch(
"""
<p>
Some text
<img width="965" height="320" src="https://localhost/an-image.png" alt="alt-desc" style="width:100%;">
<iframe src="url" style="width:100%;"></iframe>
aaa
</p>
""".lines(), result.lines());
}
}
"""
.lines(),
result.lines());
}
}

View File

@@ -1,267 +1,321 @@
package com.commafeed.backend.service;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
class FeedEntryFilteringServiceTest {
private CommaFeedConfiguration config;
private CommaFeedConfiguration config;
private FeedEntryFilteringService service;
private FeedEntryFilteringService service;
private FeedEntry entry;
private FeedEntry entry;
@BeforeEach
void init() {
config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout()).thenReturn(Duration.ofSeconds(30));
@BeforeEach
void init() {
config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.feedRefresh().filteringExpressionEvaluationTimeout())
.thenReturn(Duration.ofSeconds(30));
service = new FeedEntryFilteringService(config);
service = new FeedEntryFilteringService(config);
entry = new FeedEntry();
entry.setUrl("https://github.com/Athou/commafeed");
entry = new FeedEntry();
entry.setUrl("https://github.com/Athou/commafeed");
FeedEntryContent content = new FeedEntryContent();
content.setAuthor("Athou");
content.setTitle("Merge pull request #662 from Athou/dw8");
content.setContent("Merge pull request #662 from Athou/dw8");
entry.setContent(content);
FeedEntryContent content = new FeedEntryContent();
content.setAuthor("Athou");
content.setTitle("Merge pull request #662 from Athou/dw8");
content.setContent("Merge pull request #662 from Athou/dw8");
entry.setContent(content);
}
}
@Test
void emptyFilterMatchesFilter() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry(null, entry));
}
@Test
void emptyFilterMatchesFilter() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry(null, entry));
}
@Test
void blankFilterMatchesFilter() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("", entry));
}
@Test
void blankFilterMatchesFilter() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("", entry));
}
@Test
void simpleEqualsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\"", entry));
}
@Test
void simpleEqualsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\"", entry));
}
@Test
void simpleNotEqualsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author != \"other\"", entry));
}
@Test
void simpleNotEqualsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author != \"other\"", entry));
}
@Test
void containsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author.contains(\"Athou\")", entry));
}
@Test
void containsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author.contains(\"Athou\")", entry));
}
@Test
void titleContainsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("title.contains(\"Merge\")", entry));
}
@Test
void titleContainsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("title.contains(\"Merge\")", entry));
}
@Test
void urlContainsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("url.contains(\"github\")", entry));
}
@Test
void urlContainsExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("url.contains(\"github\")", entry));
}
@Test
void andExpression() throws FeedEntryFilterException {
Assertions.assertTrue(
service.filterMatchesEntry(
"author == \"Athou\" && url.contains(\"github\")", entry));
}
@Test
void andExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author == \"Athou\" && url.contains(\"github\")", entry));
}
@Test
void orExpression() throws FeedEntryFilterException {
Assertions.assertTrue(
service.filterMatchesEntry(
"author == \"other\" || url.contains(\"github\")", entry));
}
@Test
void orExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("author == \"other\" || url.contains(\"github\")", entry));
}
@Test
void notExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("!(author == \"other\")", entry));
}
@Test
void notExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("!(author == \"other\")", entry));
}
@Test
void incorrectExpressionThrowsException() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("not valid cel", entry));
}
@Test
void incorrectExpressionThrowsException() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("not valid cel", entry));
}
@Test
void falseValueReturnsFalse() throws FeedEntryFilterException {
Assertions.assertFalse(service.filterMatchesEntry("false", entry));
}
@Test
void falseValueReturnsFalse() throws FeedEntryFilterException {
Assertions.assertFalse(service.filterMatchesEntry("false", entry));
}
@Test
void trueValueReturnsTrue() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("true", entry));
}
@Test
void trueValueReturnsTrue() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("true", entry));
}
@Test
void startsWithExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("title.startsWith(\"Merge\")", entry));
}
@Test
void startsWithExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("title.startsWith(\"Merge\")", entry));
}
@Test
void endsWithExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("url.endsWith(\"commafeed\")", entry));
}
@Test
void endsWithExpression() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("url.endsWith(\"commafeed\")", entry));
}
@Test
void categoriesContainsExpression() throws FeedEntryFilterException {
FeedEntryContent content = entry.getContent();
content.setCategories("tech, programming, java");
entry.setContent(content);
Assertions.assertTrue(
service.filterMatchesEntry("categories.contains(\"programming\")", entry));
}
@Test
void categoriesContainsExpression() throws FeedEntryFilterException {
FeedEntryContent content = entry.getContent();
content.setCategories("tech, programming, java");
entry.setContent(content);
Assertions.assertTrue(service.filterMatchesEntry("categories.contains(\"programming\")", entry));
}
@Test
void caseInsensitiveAuthorMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("authorLower == \"athou\"", entry));
}
@Test
void caseInsensitiveAuthorMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("authorLower == \"athou\"", entry));
}
@Test
void caseInsensitiveTitleMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("titleLower.contains(\"merge\")", entry));
}
@Test
void caseInsensitiveTitleMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("titleLower.contains(\"merge\")", entry));
}
@Test
void caseInsensitiveUrlMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("urlLower.contains(\"github\")", entry));
}
@Test
void caseInsensitiveUrlMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("urlLower.contains(\"github\")", entry));
}
@Test
void caseInsensitiveContentMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(
service.filterMatchesEntry("contentLower.contains(\"merge\")", entry));
}
@Test
void caseInsensitiveContentMatchUsingLowerVariable() throws FeedEntryFilterException {
Assertions.assertTrue(service.filterMatchesEntry("contentLower.contains(\"merge\")", entry));
}
@Test
void caseInsensitiveCategoriesMatchUsingLowerVariable() throws FeedEntryFilterException {
FeedEntryContent content = entry.getContent();
content.setCategories("Tech, Programming, Java");
entry.setContent(content);
Assertions.assertTrue(
service.filterMatchesEntry("categoriesLower.contains(\"tech\")", entry));
}
@Test
void caseInsensitiveCategoriesMatchUsingLowerVariable() throws FeedEntryFilterException {
FeedEntryContent content = entry.getContent();
content.setCategories("Tech, Programming, Java");
entry.setContent(content);
Assertions.assertTrue(service.filterMatchesEntry("categoriesLower.contains(\"tech\")", entry));
}
@Nested
class Sandbox {
@Nested
class Sandbox {
@Test
void sandboxBlocksSystemPropertyAccess() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"java.lang.System.getProperty(\"user.home\")", entry));
}
@Test
void sandboxBlocksSystemPropertyAccess() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.System.getProperty(\"user.home\")", entry));
}
@Test
void sandboxBlocksRuntimeExec() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"java.lang.Runtime.getRuntime().exec(\"calc\")", entry));
}
@Test
void sandboxBlocksRuntimeExec() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.Runtime.getRuntime().exec(\"calc\")", entry));
}
@Test
void sandboxBlocksProcessBuilder() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"new java.lang.ProcessBuilder(\"cmd\").start()", entry));
}
@Test
void sandboxBlocksProcessBuilder() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new java.lang.ProcessBuilder(\"cmd\").start()", entry));
}
@Test
void sandboxBlocksClassLoading() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"java.lang.Class.forName(\"java.lang.Runtime\")", entry));
}
@Test
void sandboxBlocksClassLoading() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.Class.forName(\"java.lang.Runtime\")", entry));
}
@Test
void sandboxBlocksReflection() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("title.getClass().getMethods()", entry));
}
@Test
void sandboxBlocksReflection() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("title.getClass().getMethods()", entry));
}
@Test
void sandboxBlocksFileAccess() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"new java.io.File(\"/etc/passwd\").exists()", entry));
}
@Test
void sandboxBlocksFileAccess() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new java.io.File(\"/etc/passwd\").exists()", entry));
}
@Test
void sandboxBlocksFileRead() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"java.nio.file.Files.readString(java.nio.file.Paths.get(\"/etc/passwd\"))",
entry));
}
@Test
void sandboxBlocksFileRead() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.nio.file.Files.readString(java.nio.file.Paths.get(\"/etc/passwd\"))", entry));
}
@Test
void sandboxBlocksNetworkAccess() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"new java.net.URL(\"http://evil.com\").openConnection()",
entry));
}
@Test
void sandboxBlocksNetworkAccess() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new java.net.URL(\"http://evil.com\").openConnection()", entry));
}
@Test
void sandboxBlocksScriptEngine() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() ->
service.filterMatchesEntry(
"new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"1+1\")",
entry));
}
@Test
void sandboxBlocksScriptEngine() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service
.filterMatchesEntry("new javax.script.ScriptEngineManager().getEngineByName(\"js\").eval(\"1+1\")", entry));
}
@Test
void sandboxBlocksThreadCreation() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new java.lang.Thread().start()", entry));
}
@Test
void sandboxBlocksThreadCreation() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new java.lang.Thread().start()", entry));
}
@Test
void sandboxBlocksEnvironmentVariableAccess() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.System.getenv(\"PATH\")", entry));
}
@Test
void sandboxBlocksEnvironmentVariableAccess() {
Assertions.assertThrows(FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.System.getenv(\"PATH\")", entry));
}
@Test
void sandboxBlocksExitCall() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.lang.System.exit(0)", entry));
}
@Test
void sandboxBlocksExitCall() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.lang.System.exit(0)", entry));
}
@Test
void sandboxBlocksUndeclaredVariables() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("unknownVariable == \"test\"", entry));
}
@Test
void sandboxBlocksUndeclaredVariables() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("unknownVariable == \"test\"", entry));
}
@Test
void sandboxBlocksMethodInvocationOnStrings() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("title.toCharArray()", entry));
}
@Test
void sandboxBlocksMethodInvocationOnStrings() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.toCharArray()", entry));
}
@Test
void sandboxBlocksArbitraryJavaMethodCalls() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("title.getBytes()", entry));
}
@Test
void sandboxBlocksArbitraryJavaMethodCalls() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("title.getBytes()", entry));
}
@Test
void sandboxOnlyAllowsDeclaredVariables() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("System", entry));
}
@Test
void sandboxOnlyAllowsDeclaredVariables() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("System", entry));
}
@Test
void sandboxBlocksConstructorCalls() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("new String(\"test\")", entry));
}
@Test
void sandboxBlocksConstructorCalls() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("new String(\"test\")", entry));
}
@Test
void sandboxBlocksStaticMethodCalls() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("String.valueOf(123)", entry));
}
@Test
void sandboxBlocksStaticMethodCalls() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("String.valueOf(123)", entry));
}
@Test
void sandboxBlocksLambdaExpressions() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("() -> true", entry));
}
@Test
void sandboxBlocksObjectInstantiation() {
Assertions.assertThrows(FeedEntryFilterException.class, () -> service.filterMatchesEntry("java.util.HashMap{}", entry));
}
}
@Test
void sandboxBlocksLambdaExpressions() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("() -> true", entry));
}
@Test
void sandboxBlocksObjectInstantiation() {
Assertions.assertThrows(
FeedEntryFilterException.class,
() -> service.filterMatchesEntry("java.util.HashMap{}", entry));
}
}
}

View File

@@ -1,10 +1,11 @@
package com.commafeed.backend.service;
import com.commafeed.backend.favicon.Favicon;
import com.commafeed.backend.favicon.FaviconFetcher;
import com.commafeed.backend.model.Feed;
import jakarta.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.List;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -13,139 +14,133 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.backend.favicon.Favicon;
import com.commafeed.backend.favicon.FaviconFetcher;
import com.commafeed.backend.model.Feed;
@ExtendWith(MockitoExtension.class)
class FeedFaviconServiceTest {
@Mock
private FaviconFetcher fetcher1;
@Mock private FaviconFetcher fetcher1;
@Mock
private FaviconFetcher fetcher2;
@Mock private FaviconFetcher fetcher2;
private FeedFaviconService service;
private Feed feed;
private FeedFaviconService service;
private Feed feed;
@BeforeEach
void init() throws IOException {
service = new FeedFaviconService(List.of(fetcher1, fetcher2));
feed = new Feed();
feed.setUrl("https://example.com/feed");
}
@BeforeEach
void init() throws IOException {
service = new FeedFaviconService(List.of(fetcher1, fetcher2));
feed = new Feed();
feed.setUrl("https://example.com/feed");
}
@Test
void testReturnsFirstValidFavicon() {
byte[] iconBytes = new byte[1000];
Favicon validFavicon = new Favicon(iconBytes, "image/png");
@Test
void testReturnsFirstValidFavicon() {
byte[] iconBytes = new byte[1000];
Favicon validFavicon = new Favicon(iconBytes, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
Mockito.verify(fetcher1).fetch(feed);
Mockito.verifyNoInteractions(fetcher2);
}
Assertions.assertEquals(validFavicon, result);
Mockito.verify(fetcher1).fetch(feed);
Mockito.verifyNoInteractions(fetcher2);
}
@Test
void testFallsBackToNextFetcherWhenFirstReturnsNull() {
byte[] iconBytes = new byte[1000];
Favicon validFavicon = new Favicon(iconBytes, "image/png");
@Test
void testFallsBackToNextFetcherWhenFirstReturnsNull() {
byte[] iconBytes = new byte[1000];
Favicon validFavicon = new Favicon(iconBytes, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(null);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(null);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
}
Assertions.assertEquals(validFavicon, result);
}
@Test
void testFallsBackToNextFetcherWhenFirstReturnsTooSmallIcon() {
byte[] tinyIcon = new byte[50];
Favicon tinyFavicon = new Favicon(tinyIcon, "image/png");
@Test
void testFallsBackToNextFetcherWhenFirstReturnsTooSmallIcon() {
byte[] tinyIcon = new byte[50];
Favicon tinyFavicon = new Favicon(tinyIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(tinyFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(tinyFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
}
Assertions.assertEquals(validFavicon, result);
}
@Test
void testFallsBackToNextFetcherWhenFirstReturnsTooLargeIcon() {
byte[] hugeIcon = new byte[100001];
Favicon hugeFavicon = new Favicon(hugeIcon, "image/png");
@Test
void testFallsBackToNextFetcherWhenFirstReturnsTooLargeIcon() {
byte[] hugeIcon = new byte[100001];
Favicon hugeFavicon = new Favicon(hugeIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(hugeFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(hugeFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
}
Assertions.assertEquals(validFavicon, result);
}
@Test
void testFallsBackToNextFetcherWhenFirstReturnsBlacklistedContentType() {
byte[] iconBytes = new byte[1000];
Favicon xmlFavicon = new Favicon(iconBytes, "application/xml");
@Test
void testFallsBackToNextFetcherWhenFirstReturnsBlacklistedContentType() {
byte[] iconBytes = new byte[1000];
Favicon xmlFavicon = new Favicon(iconBytes, "application/xml");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(xmlFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(xmlFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
}
Assertions.assertEquals(validFavicon, result);
}
@Test
void testFallsBackToNextFetcherWhenFirstReturnsHtmlContentType() {
byte[] iconBytes = new byte[1000];
Favicon htmlFavicon = new Favicon(iconBytes, "text/html");
@Test
void testFallsBackToNextFetcherWhenFirstReturnsHtmlContentType() {
byte[] iconBytes = new byte[1000];
Favicon htmlFavicon = new Favicon(iconBytes, "text/html");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
byte[] validIcon = new byte[1000];
Favicon validFavicon = new Favicon(validIcon, "image/png");
Mockito.when(fetcher1.fetch(feed)).thenReturn(htmlFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Mockito.when(fetcher1.fetch(feed)).thenReturn(htmlFavicon);
Mockito.when(fetcher2.fetch(feed)).thenReturn(validFavicon);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertEquals(validFavicon, result);
}
Assertions.assertEquals(validFavicon, result);
}
@Test
void testReturnsDefaultFaviconWhenAllFetchersFail() {
Mockito.when(fetcher1.fetch(feed)).thenReturn(null);
Mockito.when(fetcher2.fetch(feed)).thenReturn(null);
@Test
void testReturnsDefaultFaviconWhenAllFetchersFail() {
Mockito.when(fetcher1.fetch(feed)).thenReturn(null);
Mockito.when(fetcher2.fetch(feed)).thenReturn(null);
Favicon result = service.fetchFavicon(feed);
Favicon result = service.fetchFavicon(feed);
Assertions.assertNotNull(result);
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif")));
Assertions.assertTrue(result.icon().length > 0);
}
Assertions.assertNotNull(result);
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif")));
Assertions.assertTrue(result.icon().length > 0);
}
@Test
void testReturnsDefaultFaviconWhenNoFetchersRegistered() throws IOException {
FeedFaviconService emptyService = new FeedFaviconService(List.of());
@Test
void testReturnsDefaultFaviconWhenNoFetchersRegistered() throws IOException {
FeedFaviconService emptyService = new FeedFaviconService(List.of());
Favicon result = emptyService.fetchFavicon(feed);
Favicon result = emptyService.fetchFavicon(feed);
Assertions.assertNotNull(result);
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif")));
}
Assertions.assertNotNull(result);
Assertions.assertTrue(result.mediaType().isCompatible(MediaType.valueOf("image/gif")));
}
}

View File

@@ -1,23 +1,24 @@
package com.commafeed.backend.service;
import java.util.HexFormat;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class PasswordEncryptionServiceTest {
@Test
void authenticate() {
String password = "password";
byte[] salt = "abcdefgh".getBytes();
@Test
void authenticate() {
String password = "password";
byte[] salt = "abcdefgh".getBytes();
PasswordEncryptionService passwordEncryptionService = new PasswordEncryptionService();
byte[] encryptedPassword = passwordEncryptionService.getEncryptedPassword(password, salt);
PasswordEncryptionService passwordEncryptionService = new PasswordEncryptionService();
byte[] encryptedPassword = passwordEncryptionService.getEncryptedPassword(password, salt);
// make sure the encrypted password is always the same for a fixed salt
Assertions.assertEquals("8b4660158141d9f4f7865718b9a2b940a3e3cea9", HexFormat.of().formatHex(encryptedPassword));
Assertions.assertTrue(passwordEncryptionService.authenticate(password, encryptedPassword, salt));
}
}
// make sure the encrypted password is always the same for a fixed salt
Assertions.assertEquals(
"8b4660158141d9f4f7865718b9a2b940a3e3cea9",
HexFormat.of().formatHex(encryptedPassword));
Assertions.assertTrue(
passwordEncryptionService.authenticate(password, encryptedPassword, salt));
}
}

View File

@@ -1,7 +1,16 @@
package com.commafeed.backend.service;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpClientFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.PushNotificationType;
import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings;
import java.time.Duration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -14,79 +23,79 @@ import org.mockserver.model.HttpResponse;
import org.mockserver.model.JsonBody;
import org.mockserver.model.MediaType;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.HttpClientFactory;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserSettings.PushNotificationType;
import com.commafeed.backend.model.UserSettings.PushNotificationUserSettings;
@ExtendWith(MockServerExtension.class)
class PushNotificationServiceTest {
private MockServerClient mockServerClient;
private PushNotificationService pushNotificationService;
private CommaFeedConfiguration config;
private PushNotificationUserSettings userSettings;
private FeedSubscription subscription;
private FeedEntry entry;
private MockServerClient mockServerClient;
private PushNotificationService pushNotificationService;
private CommaFeedConfiguration config;
private PushNotificationUserSettings userSettings;
private FeedSubscription subscription;
private FeedEntry entry;
@BeforeEach
void init(MockServerClient mockServerClient) {
this.mockServerClient = mockServerClient;
this.mockServerClient.reset();
@BeforeEach
void init(MockServerClient mockServerClient) {
this.mockServerClient = mockServerClient;
this.mockServerClient.reset();
this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.pushNotifications().enabled()).thenReturn(true);
Mockito.when(config.pushNotifications().threads()).thenReturn(1);
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
this.config = Mockito.mock(CommaFeedConfiguration.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(config.pushNotifications().enabled()).thenReturn(true);
Mockito.when(config.pushNotifications().threads()).thenReturn(1);
Mockito.when(config.httpClient().responseTimeout()).thenReturn(Duration.ofSeconds(30));
HttpClientFactory httpClientFactory = new HttpClientFactory(config, Mockito.mock(com.commafeed.CommaFeedVersion.class));
MetricRegistry metricRegistry = Mockito.mock(MetricRegistry.class);
Mockito.when(metricRegistry.meter(Mockito.anyString())).thenReturn(Mockito.mock(Meter.class));
HttpClientFactory httpClientFactory =
new HttpClientFactory(config, Mockito.mock(com.commafeed.CommaFeedVersion.class));
MetricRegistry metricRegistry = Mockito.mock(MetricRegistry.class);
Mockito.when(metricRegistry.meter(Mockito.anyString()))
.thenReturn(Mockito.mock(Meter.class));
this.pushNotificationService = new PushNotificationService(httpClientFactory, metricRegistry, config);
this.pushNotificationService =
new PushNotificationService(httpClientFactory, metricRegistry, config);
this.userSettings = new PushNotificationUserSettings();
this.userSettings = new PushNotificationUserSettings();
this.subscription = createSubscription("Test Feed");
this.entry = createEntry("Test Entry", "http://example.com/entry");
}
this.subscription = createSubscription("Test Feed");
this.entry = createEntry("Test Entry", "http://example.com/entry");
}
@Test
void testNtfyNotification() {
userSettings.setType(PushNotificationType.NTFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setTopic("test-topic");
userSettings.setUserSecret("test-secret");
@Test
void testNtfyNotification() {
userSettings.setType(PushNotificationType.NTFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setTopic("test-topic");
userSettings.setUserSecret("test-secret");
mockServerClient.when(HttpRequest.request()
.withMethod("POST")
.withPath("/test-topic")
.withHeader("Title", "Test Feed")
.withHeader("Click", "http://example.com/entry")
.withHeader("Authorization", "Bearer test-secret")
.withBody("Test Entry")).respond(HttpResponse.response().withStatusCode(200));
mockServerClient
.when(
HttpRequest.request()
.withMethod("POST")
.withPath("/test-topic")
.withHeader("Title", "Test Feed")
.withHeader("Click", "http://example.com/entry")
.withHeader("Authorization", "Bearer test-secret")
.withBody("Test Entry"))
.respond(HttpResponse.response().withStatusCode(200));
Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
}
Assertions.assertDoesNotThrow(
() -> pushNotificationService.notify(userSettings, subscription, entry));
}
@Test
void testGotifyNotification() {
userSettings.setType(PushNotificationType.GOTIFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setUserSecret("gotify-token");
@Test
void testGotifyNotification() {
userSettings.setType(PushNotificationType.GOTIFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setUserSecret("gotify-token");
mockServerClient.when(HttpRequest.request()
.withMethod("POST")
.withPath("/message")
.withHeader("X-Gotify-Key", "gotify-token")
.withContentType(MediaType.APPLICATION_JSON_UTF_8)
.withBody(JsonBody.json("""
mockServerClient
.when(
HttpRequest.request()
.withMethod("POST")
.withPath("/message")
.withHeader("X-Gotify-Key", "gotify-token")
.withContentType(MediaType.APPLICATION_JSON_UTF_8)
.withBody(
JsonBody.json(
"""
{
"title": "Test Feed",
"message": "Test Entry",
@@ -99,38 +108,41 @@ class PushNotificationServiceTest {
}
}
}
"""))).respond(HttpResponse.response().withStatusCode(200));
""")))
.respond(HttpResponse.response().withStatusCode(200));
Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
}
Assertions.assertDoesNotThrow(
() -> pushNotificationService.notify(userSettings, subscription, entry));
}
@Test
void testPushNotificationDisabled() {
Mockito.when(config.pushNotifications().enabled()).thenReturn(false);
userSettings.setType(PushNotificationType.NTFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setTopic("test-topic");
@Test
void testPushNotificationDisabled() {
Mockito.when(config.pushNotifications().enabled()).thenReturn(false);
userSettings.setType(PushNotificationType.NTFY);
userSettings.setServerUrl("http://localhost:" + mockServerClient.getPort());
userSettings.setTopic("test-topic");
Assertions.assertDoesNotThrow(() -> pushNotificationService.notify(userSettings, subscription, entry));
mockServerClient.verifyZeroInteractions();
}
Assertions.assertDoesNotThrow(
() -> pushNotificationService.notify(userSettings, subscription, entry));
mockServerClient.verifyZeroInteractions();
}
private static FeedSubscription createSubscription(String title) {
FeedSubscription subscription = new FeedSubscription();
subscription.setTitle(title);
subscription.setFeed(new Feed());
private static FeedSubscription createSubscription(String title) {
FeedSubscription subscription = new FeedSubscription();
subscription.setTitle(title);
subscription.setFeed(new Feed());
return subscription;
}
return subscription;
}
private static FeedEntry createEntry(String title, String url) {
FeedEntry entry = new FeedEntry();
private static FeedEntry createEntry(String title, String url) {
FeedEntry entry = new FeedEntry();
FeedEntryContent content = new FeedEntryContent();
content.setTitle(title);
FeedEntryContent content = new FeedEntryContent();
content.setTitle(title);
entry.setContent(content);
entry.setUrl(url);
return entry;
}
entry.setContent(content);
entry.setUrl(url);
return entry;
}
}

View File

@@ -1,15 +1,5 @@
package com.commafeed.backend.service;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
@@ -18,177 +8,201 @@ import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.service.internal.PostLoginActivities;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
private static final byte[] SALT = new byte[] { 1, 2, 3 };
private static final byte[] ENCRYPTED_PASSWORD = new byte[] { 5, 6, 7 };
private static final byte[] SALT = new byte[] {1, 2, 3};
private static final byte[] ENCRYPTED_PASSWORD = new byte[] {5, 6, 7};
@Mock
private CommaFeedConfiguration commaFeedConfiguration;
@Mock
private FeedCategoryDAO feedCategoryDAO;
@Mock
private FeedSubscriptionDAO feedSubscriptionDAO;
@Mock
private UserDAO userDAO;
@Mock
private UserSettingsDAO userSettingsDAO;
@Mock
private UserRoleDAO userRoleDAO;
@Mock
private PasswordEncryptionService passwordEncryptionService;
@Mock
private PostLoginActivities postLoginActivities;
@Mock private CommaFeedConfiguration commaFeedConfiguration;
@Mock private FeedCategoryDAO feedCategoryDAO;
@Mock private FeedSubscriptionDAO feedSubscriptionDAO;
@Mock private UserDAO userDAO;
@Mock private UserSettingsDAO userSettingsDAO;
@Mock private UserRoleDAO userRoleDAO;
@Mock private PasswordEncryptionService passwordEncryptionService;
@Mock private PostLoginActivities postLoginActivities;
private User disabledUser;
private User normalUser;
private User disabledUser;
private User normalUser;
private UserService userService;
private UserService userService;
@BeforeEach
void init() {
userService = new UserService(feedCategoryDAO, feedSubscriptionDAO, userDAO, userRoleDAO, userSettingsDAO,
passwordEncryptionService, commaFeedConfiguration, postLoginActivities);
@BeforeEach
void init() {
userService =
new UserService(
feedCategoryDAO,
feedSubscriptionDAO,
userDAO,
userRoleDAO,
userSettingsDAO,
passwordEncryptionService,
commaFeedConfiguration,
postLoginActivities);
disabledUser = new User();
disabledUser.setDisabled(true);
disabledUser = new User();
disabledUser.setDisabled(true);
normalUser = new User();
normalUser.setDisabled(false);
normalUser.setSalt(SALT);
normalUser.setPassword(ENCRYPTED_PASSWORD);
}
normalUser = new User();
normalUser.setDisabled(false);
normalUser.setSalt(SALT);
normalUser.setPassword(ENCRYPTED_PASSWORD);
}
@Test
void callingLoginShouldNotReturnUserObjectWhenGivenNullNameOrEmail() {
Optional<User> user = userService.login(null, "password");
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldNotReturnUserObjectWhenGivenNullNameOrEmail() {
Optional<User> user = userService.login(null, "password");
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldNotReturnUserObjectWhenGivenNullPassword() {
Optional<User> user = userService.login("testusername", null);
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldNotReturnUserObjectWhenGivenNullPassword() {
Optional<User> user = userService.login("testusername", null);
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldLookupUserByName() {
userService.login("test", "password");
Mockito.verify(userDAO).findByName("test");
}
@Test
void callingLoginShouldLookupUserByName() {
userService.login("test", "password");
Mockito.verify(userDAO).findByName("test");
}
@Test
void callingLoginShouldLookupUserByEmailIfLookupByNameFailed() {
Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null);
userService.login("test@test.com", "password");
Mockito.verify(userDAO).findByEmail("test@test.com");
}
@Test
void callingLoginShouldLookupUserByEmailIfLookupByNameFailed() {
Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null);
userService.login("test@test.com", "password");
Mockito.verify(userDAO).findByEmail("test@test.com");
}
@Test
void callingLoginShouldNotReturnUserObjectIfCouldNotFindUserByNameOrEmail() {
Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null);
Mockito.when(userDAO.findByEmail("test@test.com")).thenReturn(null);
@Test
void callingLoginShouldNotReturnUserObjectIfCouldNotFindUserByNameOrEmail() {
Mockito.when(userDAO.findByName("test@test.com")).thenReturn(null);
Mockito.when(userDAO.findByEmail("test@test.com")).thenReturn(null);
Optional<User> user = userService.login("test@test.com", "password");
Optional<User> user = userService.login("test@test.com", "password");
Assertions.assertFalse(user.isPresent());
}
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldNotReturnUserObjectIfUserIsDisabled() {
Mockito.when(userDAO.findByName("test")).thenReturn(disabledUser);
Optional<User> user = userService.login("test", "password");
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldNotReturnUserObjectIfUserIsDisabled() {
Mockito.when(userDAO.findByName("test")).thenReturn(disabledUser);
Optional<User> user = userService.login("test", "password");
Assertions.assertFalse(user.isPresent());
}
@Test
void callingLoginShouldTryToAuthenticateUserWhoIsNotDisabled() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class)))
.thenReturn(false);
@Test
void callingLoginShouldTryToAuthenticateUserWhoIsNotDisabled() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(
passwordEncryptionService.authenticate(
Mockito.anyString(),
Mockito.any(byte[].class),
Mockito.any(byte[].class)))
.thenReturn(false);
userService.login("test", "password");
userService.login("test", "password");
Mockito.verify(passwordEncryptionService).authenticate("password", ENCRYPTED_PASSWORD, SALT);
}
Mockito.verify(passwordEncryptionService)
.authenticate("password", ENCRYPTED_PASSWORD, SALT);
}
@Test
void callingLoginShouldNotReturnUserObjectOnUnsuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class)))
.thenReturn(false);
@Test
void callingLoginShouldNotReturnUserObjectOnUnsuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(
passwordEncryptionService.authenticate(
Mockito.anyString(),
Mockito.any(byte[].class),
Mockito.any(byte[].class)))
.thenReturn(false);
Optional<User> authenticatedUser = userService.login("test", "password");
Optional<User> authenticatedUser = userService.login("test", "password");
Assertions.assertFalse(authenticatedUser.isPresent());
}
Assertions.assertFalse(authenticatedUser.isPresent());
}
@Test
void callingLoginShouldExecutePostLoginActivitiesForUserOnSuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class)))
.thenReturn(true);
Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class));
@Test
void callingLoginShouldExecutePostLoginActivitiesForUserOnSuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(
passwordEncryptionService.authenticate(
Mockito.anyString(),
Mockito.any(byte[].class),
Mockito.any(byte[].class)))
.thenReturn(true);
Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class));
userService.login("test", "password");
userService.login("test", "password");
Mockito.verify(postLoginActivities).executeFor(normalUser);
}
Mockito.verify(postLoginActivities).executeFor(normalUser);
}
@Test
void callingLoginShouldReturnUserObjectOnSuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(passwordEncryptionService.authenticate(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class)))
.thenReturn(true);
Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class));
@Test
void callingLoginShouldReturnUserObjectOnSuccessfulAuthentication() {
Mockito.when(userDAO.findByName("test")).thenReturn(normalUser);
Mockito.when(
passwordEncryptionService.authenticate(
Mockito.anyString(),
Mockito.any(byte[].class),
Mockito.any(byte[].class)))
.thenReturn(true);
Mockito.doNothing().when(postLoginActivities).executeFor(Mockito.any(User.class));
Optional<User> authenticatedUser = userService.login("test", "password");
Optional<User> authenticatedUser = userService.login("test", "password");
Assertions.assertTrue(authenticatedUser.isPresent());
Assertions.assertEquals(normalUser, authenticatedUser.get());
}
Assertions.assertTrue(authenticatedUser.isPresent());
Assertions.assertEquals(normalUser, authenticatedUser.get());
}
@Test
void apiLoginShouldNotReturnUserIfApikeyNull() {
Optional<User> user = userService.login(null);
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldNotReturnUserIfApikeyNull() {
Optional<User> user = userService.login(null);
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldLookupUserByApikey() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null);
userService.login("apikey");
Mockito.verify(userDAO).findByApiKey("apikey");
}
@Test
void apiLoginShouldLookupUserByApikey() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null);
userService.login("apikey");
Mockito.verify(userDAO).findByApiKey("apikey");
}
@Test
void apiLoginShouldNotReturnUserIfUserNotFoundFromLookupByApikey() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null);
Optional<User> user = userService.login("apikey");
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldNotReturnUserIfUserNotFoundFromLookupByApikey() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(null);
Optional<User> user = userService.login("apikey");
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldNotReturnUserIfUserFoundFromApikeyLookupIsDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(disabledUser);
Optional<User> user = userService.login("apikey");
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldNotReturnUserIfUserFoundFromApikeyLookupIsDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(disabledUser);
Optional<User> user = userService.login("apikey");
Assertions.assertFalse(user.isPresent());
}
@Test
void apiLoginShouldPerformPostLoginActivitiesIfUserFoundFromApikeyLookupNotDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser);
userService.login("apikey");
Mockito.verify(postLoginActivities).executeFor(normalUser);
}
@Test
void apiLoginShouldReturnUserIfUserFoundFromApikeyLookupNotDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser);
Optional<User> returnedUser = userService.login("apikey");
Assertions.assertEquals(Optional.of(normalUser), returnedUser);
}
@Test
void apiLoginShouldPerformPostLoginActivitiesIfUserFoundFromApikeyLookupNotDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser);
userService.login("apikey");
Mockito.verify(postLoginActivities).executeFor(normalUser);
}
@Test
void apiLoginShouldReturnUserIfUserFoundFromApikeyLookupNotDisabled() {
Mockito.when(userDAO.findByApiKey("apikey")).thenReturn(normalUser);
Optional<User> returnedUser = userService.login("apikey");
Assertions.assertEquals(Optional.of(normalUser), returnedUser);
}
}

View File

@@ -1,19 +1,5 @@
package com.commafeed.backend.service.db;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration;
@@ -24,130 +10,146 @@ import com.commafeed.backend.dao.FeedEntryDAO.FeedCapacity;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.Callable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class DatabaseCleaningServiceTest {
private static final int BATCH_SIZE = 100;
private static final int BATCH_SIZE = 100;
@Mock
private CommaFeedConfiguration config;
@Mock private CommaFeedConfiguration config;
@Mock
private CommaFeedConfiguration.Database databaseConfig;
@Mock private CommaFeedConfiguration.Database databaseConfig;
@Mock
private CommaFeedConfiguration.Database.Cleanup cleaningConfig;
@Mock private CommaFeedConfiguration.Database.Cleanup cleaningConfig;
@Mock
private UnitOfWork unitOfWork;
@Mock private UnitOfWork unitOfWork;
@Mock
private FeedDAO feedDAO;
@Mock private FeedDAO feedDAO;
@Mock
private FeedEntryDAO feedEntryDAO;
@Mock private FeedEntryDAO feedEntryDAO;
@Mock
private FeedEntryContentDAO feedEntryContentDAO;
@Mock private FeedEntryContentDAO feedEntryContentDAO;
@Mock
private FeedEntryStatusDAO feedEntryStatusDAO;
@Mock private FeedEntryStatusDAO feedEntryStatusDAO;
@Mock
private MetricRegistry metrics;
@Mock private MetricRegistry metrics;
@Mock
private Meter entriesDeletedMeter;
@Mock private Meter entriesDeletedMeter;
private DatabaseCleaningService service;
private DatabaseCleaningService service;
@BeforeEach
void setUp() {
Mockito.when(config.database()).thenReturn(databaseConfig);
Mockito.when(databaseConfig.cleanup()).thenReturn(cleaningConfig);
Mockito.when(cleaningConfig.batchSize()).thenReturn(BATCH_SIZE);
Mockito.when(metrics.meter(Mockito.anyString())).thenReturn(entriesDeletedMeter);
@BeforeEach
void setUp() {
Mockito.when(config.database()).thenReturn(databaseConfig);
Mockito.when(databaseConfig.cleanup()).thenReturn(cleaningConfig);
Mockito.when(cleaningConfig.batchSize()).thenReturn(BATCH_SIZE);
Mockito.when(metrics.meter(Mockito.anyString())).thenReturn(entriesDeletedMeter);
Mockito.when(unitOfWork.call(Mockito.any())).thenAnswer(invocation -> ((Callable<?>) invocation.getArgument(0)).call());
Mockito.when(unitOfWork.call(Mockito.any()))
.thenAnswer(invocation -> ((Callable<?>) invocation.getArgument(0)).call());
service = new DatabaseCleaningService(config, unitOfWork, feedDAO, feedEntryDAO, feedEntryContentDAO, feedEntryStatusDAO, metrics);
}
service =
new DatabaseCleaningService(
config,
unitOfWork,
feedDAO,
feedEntryDAO,
feedEntryContentDAO,
feedEntryStatusDAO,
metrics);
}
@Test
void cleanFeedsWithoutSubscriptionsDeletesFeedsAndEntries() {
Feed feed1 = Mockito.mock(Feed.class);
Feed feed2 = Mockito.mock(Feed.class);
Mockito.when(feed1.getId()).thenReturn(1L);
Mockito.when(feed2.getId()).thenReturn(2L);
@Test
void cleanFeedsWithoutSubscriptionsDeletesFeedsAndEntries() {
Feed feed1 = Mockito.mock(Feed.class);
Feed feed2 = Mockito.mock(Feed.class);
Mockito.when(feed1.getId()).thenReturn(1L);
Mockito.when(feed2.getId()).thenReturn(2L);
// First iteration returns feeds, second returns empty list to terminate loop
Mockito.when(feedDAO.findWithoutSubscriptions(Mockito.anyInt()))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
// First iteration returns feeds, second returns empty list to terminate loop
Mockito.when(feedDAO.findWithoutSubscriptions(Mockito.anyInt()))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
Mockito.when(feedEntryDAO.delete(1L, BATCH_SIZE)).thenReturn(10, 0);
Mockito.when(feedEntryDAO.delete(2L, BATCH_SIZE)).thenReturn(5, 0);
Mockito.when(feedDAO.delete(Mockito.anyList())).thenReturn(2, 0);
Mockito.when(feedEntryDAO.delete(1L, BATCH_SIZE)).thenReturn(10, 0);
Mockito.when(feedEntryDAO.delete(2L, BATCH_SIZE)).thenReturn(5, 0);
Mockito.when(feedDAO.delete(Mockito.anyList())).thenReturn(2, 0);
service.cleanFeedsWithoutSubscriptions();
service.cleanFeedsWithoutSubscriptions();
Mockito.verify(entriesDeletedMeter, Mockito.times(4)).mark(Mockito.anyLong());
Mockito.verify(feedDAO, Mockito.times(2)).delete(Mockito.anyList());
}
Mockito.verify(entriesDeletedMeter, Mockito.times(4)).mark(Mockito.anyLong());
Mockito.verify(feedDAO, Mockito.times(2)).delete(Mockito.anyList());
}
@Test
void cleanContentsWithoutEntriesDeletesContents() {
Mockito.when(feedEntryContentDAO.deleteWithoutEntries(Mockito.anyInt())).thenReturn(50L, 30L, 0L);
@Test
void cleanContentsWithoutEntriesDeletesContents() {
Mockito.when(feedEntryContentDAO.deleteWithoutEntries(Mockito.anyInt()))
.thenReturn(50L, 30L, 0L);
service.cleanContentsWithoutEntries();
service.cleanContentsWithoutEntries();
Mockito.verify(feedEntryContentDAO, Mockito.times(3)).deleteWithoutEntries(Mockito.anyInt());
}
Mockito.verify(feedEntryContentDAO, Mockito.times(3))
.deleteWithoutEntries(Mockito.anyInt());
}
@Test
void cleanEntriesForFeedsExceedingCapacityDeletesOldEntries() {
FeedCapacity feed1 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed1.id()).thenReturn(1L);
Mockito.when(feed1.capacity()).thenReturn(180L);
@Test
void cleanEntriesForFeedsExceedingCapacityDeletesOldEntries() {
FeedCapacity feed1 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed1.id()).thenReturn(1L);
Mockito.when(feed1.capacity()).thenReturn(180L);
FeedCapacity feed2 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed2.id()).thenReturn(2L);
Mockito.when(feed2.capacity()).thenReturn(120L);
FeedCapacity feed2 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed2.id()).thenReturn(2L);
Mockito.when(feed2.capacity()).thenReturn(120L);
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50);
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50);
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70);
service.cleanEntriesForFeedsExceedingCapacity(50);
service.cleanEntriesForFeedsExceedingCapacity(50);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
@Test
void cleanEntriesOlderThanDeletesOldEntries() {
Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC);
@Test
void cleanEntriesOlderThanDeletesOldEntries() {
Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC);
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false)).thenReturn(100, 50, 0);
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false))
.thenReturn(100, 50, 0);
service.cleanEntriesOlderThan(cutoff);
service.cleanEntriesOlderThan(cutoff);
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE, false);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
Mockito.verify(feedEntryDAO, Mockito.times(3))
.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
@Test
void cleanStatusesOlderThanDeletesOldStatuses() {
Instant cutoff = LocalDate.now().minusDays(60).atStartOfDay().toInstant(ZoneOffset.UTC);
@Test
void cleanStatusesOlderThanDeletesOldStatuses() {
Instant cutoff = LocalDate.now().minusDays(60).atStartOfDay().toInstant(ZoneOffset.UTC);
Mockito.when(feedEntryStatusDAO.deleteOldStatuses(cutoff, BATCH_SIZE)).thenReturn(200L, 100L, 0L);
Mockito.when(feedEntryStatusDAO.deleteOldStatuses(cutoff, BATCH_SIZE))
.thenReturn(200L, 100L, 0L);
service.cleanStatusesOlderThan(cutoff);
service.cleanStatusesOlderThan(cutoff);
Mockito.verify(feedEntryStatusDAO, Mockito.times(3)).deleteOldStatuses(cutoff, BATCH_SIZE);
}
}
Mockito.verify(feedEntryStatusDAO, Mockito.times(3)).deleteOldStatuses(cutoff, BATCH_SIZE);
}
}

View File

@@ -1,18 +1,18 @@
package com.commafeed.backend.urlprovider;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class InPageReferenceFeedURLProviderTest {
private final InPageReferenceFeedURLProvider provider = new InPageReferenceFeedURLProvider();
private final InPageReferenceFeedURLProvider provider = new InPageReferenceFeedURLProvider();
@Test
void extractUrls() {
String url = "http://example.com";
String html = """
@Test
void extractUrls() {
String url = "http://example.com";
String html =
"""
<html>
<head>
<link type="application/atom+xml" href="/feed.atom">
@@ -22,24 +22,28 @@ class InPageReferenceFeedURLProviderTest {
</body>
</html>""";
Assertions.assertIterableEquals(List.of("http://example.com/feed.atom", "http://example.com/feed.rss"), provider.get(url, html));
}
Assertions.assertIterableEquals(
List.of("http://example.com/feed.atom", "http://example.com/feed.rss"),
provider.get(url, html));
}
@Test
void returnsEmptyListForNonHtmlContent() {
String url = "http://example.com";
String html = """
@Test
void returnsEmptyListForNonHtmlContent() {
String url = "http://example.com";
String html =
"""
<?xml version="1.0"?>
<feed></feed>
</xml>""";
Assertions.assertTrue(provider.get(url, html).isEmpty());
}
Assertions.assertTrue(provider.get(url, html).isEmpty());
}
@Test
void returnsEmptyListForHtmlWithoutFeedLinks() {
String url = "http://example.com";
String html = """
@Test
void returnsEmptyListForHtmlWithoutFeedLinks() {
String url = "http://example.com";
String html =
"""
<html>
<head>
<link type="text/css" href="/style.css">
@@ -48,6 +52,6 @@ class InPageReferenceFeedURLProviderTest {
</body>
</html>""";
Assertions.assertTrue(provider.get(url, html).isEmpty());
}
}
Assertions.assertTrue(provider.get(url, html).isEmpty());
}
}

View File

@@ -1,24 +1,24 @@
package com.commafeed.backend.urlprovider;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class YoutubeFeedURLProviderTest {
private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider();
private final YoutubeFeedURLProvider provider = new YoutubeFeedURLProvider();
@Test
void matchesYoutubeChannelURL() {
Assertions.assertIterableEquals(List.of("https://www.youtube.com/feeds/videos.xml?channel_id=abc"),
provider.get("https://www.youtube.com/channel/abc", null));
}
@Test
void matchesYoutubeChannelURL() {
Assertions.assertIterableEquals(
List.of("https://www.youtube.com/feeds/videos.xml?channel_id=abc"),
provider.get("https://www.youtube.com/channel/abc", null));
}
@Test
void doesNotmatchYoutubeChannelURL() {
Assertions.assertTrue(provider.get("https://www.anothersite.com/channel/abc", null).isEmpty());
Assertions.assertTrue(provider.get("https://www.youtube.com/user/abc", null).isEmpty());
}
}
@Test
void doesNotmatchYoutubeChannelURL() {
Assertions.assertTrue(
provider.get("https://www.anothersite.com/channel/abc", null).isEmpty());
Assertions.assertTrue(provider.get("https://www.youtube.com/user/abc", null).isEmpty());
}
}