normalize line endings

This commit is contained in:
Athou
2025-03-10 08:38:21 +01:00
parent ec4554c76e
commit fb7f041454
223 changed files with 18091 additions and 18093 deletions

View File

@@ -1,132 +1,132 @@
package com.commafeed.integration;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.mockserver.client.MockServerClient;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.SubscribeRequest;
import io.restassured.RestAssured;
import io.restassured.http.Header;
import lombok.Getter;
@Getter
public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
private MockServerClient mockServerClient;
private Client client;
private String feedUrl;
private String baseUrl;
private String apiBaseUrl;
private String webSocketUrl;
@BeforeEach
void init() throws IOException {
this.mockServerClient = ClientAndServer.startClientAndServer(0);
this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/";
this.baseUrl = "http://localhost:8085/";
this.apiBaseUrl = this.baseUrl + "rest/";
this.webSocketUrl = "ws://localhost:8085/ws";
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml"));
this.mockServerClient.when(FEED_REQUEST)
.respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
@AfterEach
void cleanup() {
if (this.mockServerClient != null) {
this.mockServerClient.close();
}
if (this.client != null) {
this.client.close();
}
}
protected void feedNowReturnsMoreEntries() throws IOException {
mockServerClient.clear(FEED_REQUEST);
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss_2.xml"));
mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
protected List<HttpCookie> login() {
List<Header> setCookieHeaders = RestAssured.given()
.auth()
.none()
.formParams("j_username", "admin", "j_password", "admin")
.post("j_security_check")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.headers()
.getList(HttpHeaders.SET_COOKIE);
return setCookieHeaders.stream().flatMap(h -> HttpCookie.parse(h.getValue()).stream()).toList();
}
protected Long subscribe(String feedUrl) {
SubscribeRequest subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(feedUrl);
subscribeRequest.setTitle("my title for this feed");
return RestAssured.given()
.body(subscribeRequest)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Long.class);
}
protected Long subscribeAndWaitForEntries(String feedUrl) {
Long subscriptionId = subscribe(feedUrl);
Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2);
return subscriptionId;
}
protected Subscription getSubscription(Long subscriptionId) {
return RestAssured.given()
.get("rest/feed/get/{id}", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Subscription.class);
}
protected Entries getFeedEntries(long subscriptionId) {
return RestAssured.given()
.get("rest/feed/entries?id={id}&readType=all", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Entries.class);
}
protected int forceRefreshAllFeeds() {
return RestAssured.given().get("rest/feed/refreshAll").then().extract().statusCode();
}
}
package com.commafeed.integration;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.mockserver.client.MockServerClient;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.SubscribeRequest;
import io.restassured.RestAssured;
import io.restassured.http.Header;
import lombok.Getter;
@Getter
public abstract class BaseIT {
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
private MockServerClient mockServerClient;
private Client client;
private String feedUrl;
private String baseUrl;
private String apiBaseUrl;
private String webSocketUrl;
@BeforeEach
void init() throws IOException {
this.mockServerClient = ClientAndServer.startClientAndServer(0);
this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/";
this.baseUrl = "http://localhost:8085/";
this.apiBaseUrl = this.baseUrl + "rest/";
this.webSocketUrl = "ws://localhost:8085/ws";
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml"));
this.mockServerClient.when(FEED_REQUEST)
.respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
@AfterEach
void cleanup() {
if (this.mockServerClient != null) {
this.mockServerClient.close();
}
if (this.client != null) {
this.client.close();
}
}
protected void feedNowReturnsMoreEntries() throws IOException {
mockServerClient.clear(FEED_REQUEST);
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss_2.xml"));
mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
}
protected List<HttpCookie> login() {
List<Header> setCookieHeaders = RestAssured.given()
.auth()
.none()
.formParams("j_username", "admin", "j_password", "admin")
.post("j_security_check")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.headers()
.getList(HttpHeaders.SET_COOKIE);
return setCookieHeaders.stream().flatMap(h -> HttpCookie.parse(h.getValue()).stream()).toList();
}
protected Long subscribe(String feedUrl) {
SubscribeRequest subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(feedUrl);
subscribeRequest.setTitle("my title for this feed");
return RestAssured.given()
.body(subscribeRequest)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Long.class);
}
protected Long subscribeAndWaitForEntries(String feedUrl) {
Long subscriptionId = subscribe(feedUrl);
Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2);
return subscriptionId;
}
protected Subscription getSubscription(Long subscriptionId) {
return RestAssured.given()
.get("rest/feed/get/{id}", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Subscription.class);
}
protected Entries getFeedEntries(long subscriptionId) {
return RestAssured.given()
.get("rest/feed/entries?id={id}&readType=all", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Entries.class);
}
protected int forceRefreshAllFeeds() {
return RestAssured.given().get("rest/feed/refreshAll").then().extract().statusCode();
}
}

View File

@@ -1,19 +1,19 @@
package com.commafeed.integration;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import com.google.common.net.HttpHeaders;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class CompressionIT {
@ParameterizedTest
@ValueSource(strings = { "/rest/server/get", "/openapi.json" })
void servedWithCompression(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header(HttpHeaders.CONTENT_ENCODING, "gzip");
}
}
package com.commafeed.integration;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import com.google.common.net.HttpHeaders;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class CompressionIT {
@ParameterizedTest
@ValueSource(strings = { "/rest/server/get", "/openapi.json" })
void servedWithCompression(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header(HttpHeaders.CONTENT_ENCODING, "gzip");
}
}

View File

@@ -1,139 +1,139 @@
package com.commafeed.integration;
import java.net.HttpCookie;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.ExceptionMappers.UnauthorizedResponse;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class SecurityIT extends BaseIT {
@Test
void notLoggedIn() {
UnauthorizedResponse info = RestAssured.given()
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED)
.extract()
.as(UnauthorizedResponse.class);
Assertions.assertTrue(info.allowRegistrations());
}
@Test
void formLogin() {
List<HttpCookie> cookies = login();
cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0));
RestAssured.given()
.header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK);
}
@Test
void basicAuthLogin() {
RestAssured.given().auth().preemptive().basic("admin", "admin").get("rest/user/profile").then().statusCode(HttpStatus.SC_OK);
}
@Test
void wrongPassword() {
RestAssured.given()
.auth()
.preemptive()
.basic("admin", "wrong-password")
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED);
}
@Test
void missingRole() {
RestAssured.given().auth().preemptive().basic("demo", "demo").get("rest/admin/metrics").then().statusCode(HttpStatus.SC_FORBIDDEN);
}
@Test
void apiKey() {
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword("admin");
req.setNewApiKey(true);
RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK);
// fetch api key
String apiKey = RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(UserModel.class)
.getApiKey();
// subscribe to a feed
SubscribeRequest subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(getFeedUrl());
subscribeRequest.setTitle("my title for this feed");
long subscriptionId = RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.body(subscribeRequest)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Long.class);
// get entries with api key
Entries entries = RestAssured.given()
.queryParam("id", subscriptionId)
.queryParam("readType", "unread")
.queryParam("apiKey", apiKey)
.get("rest/feed/entries")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Entries.class);
Assertions.assertEquals("my title for this feed", entries.getName());
// mark entry as read and expect it won't work because it's not a GET request
MarkRequest markRequest = new MarkRequest();
markRequest.setId("1");
markRequest.setRead(true);
RestAssured.given()
.body(markRequest)
.contentType(MediaType.APPLICATION_JSON)
.queryParam("apiKey", apiKey)
.post("rest/entry/mark")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED);
}
}
package com.commafeed.integration;
import java.net.HttpCookie;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.ExceptionMappers.UnauthorizedResponse;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class SecurityIT extends BaseIT {
@Test
void notLoggedIn() {
UnauthorizedResponse info = RestAssured.given()
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED)
.extract()
.as(UnauthorizedResponse.class);
Assertions.assertTrue(info.allowRegistrations());
}
@Test
void formLogin() {
List<HttpCookie> cookies = login();
cookies.forEach(c -> Assertions.assertTrue(c.getMaxAge() > 0));
RestAssured.given()
.header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK);
}
@Test
void basicAuthLogin() {
RestAssured.given().auth().preemptive().basic("admin", "admin").get("rest/user/profile").then().statusCode(HttpStatus.SC_OK);
}
@Test
void wrongPassword() {
RestAssured.given()
.auth()
.preemptive()
.basic("admin", "wrong-password")
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED);
}
@Test
void missingRole() {
RestAssured.given().auth().preemptive().basic("demo", "demo").get("rest/admin/metrics").then().statusCode(HttpStatus.SC_FORBIDDEN);
}
@Test
void apiKey() {
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword("admin");
req.setNewApiKey(true);
RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK);
// fetch api key
String apiKey = RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.get("rest/user/profile")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(UserModel.class)
.getApiKey();
// subscribe to a feed
SubscribeRequest subscribeRequest = new SubscribeRequest();
subscribeRequest.setUrl(getFeedUrl());
subscribeRequest.setTitle("my title for this feed");
long subscriptionId = RestAssured.given()
.auth()
.preemptive()
.basic("admin", "admin")
.body(subscribeRequest)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Long.class);
// get entries with api key
Entries entries = RestAssured.given()
.queryParam("id", subscriptionId)
.queryParam("readType", "unread")
.queryParam("apiKey", apiKey)
.get("rest/feed/entries")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(Entries.class);
Assertions.assertEquals("my title for this feed", entries.getName());
// mark entry as read and expect it won't work because it's not a GET request
MarkRequest markRequest = new MarkRequest();
markRequest.setId("1");
markRequest.setRead(true);
RestAssured.given()
.body(markRequest)
.contentType(MediaType.APPLICATION_JSON)
.queryParam("apiKey", apiKey)
.post("rest/entry/mark")
.then()
.statusCode(HttpStatus.SC_UNAUTHORIZED);
}
}

View File

@@ -1,23 +1,23 @@
package com.commafeed.integration;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class StaticFilesIT {
@ParameterizedTest
@ValueSource(strings = { "/", "/openapi.json", "/openapi.yaml" })
void servedWithoutCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "no-cache");
}
@ParameterizedTest
@ValueSource(strings = { "/favicon.ico" })
void servedWithCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=86400");
}
}
package com.commafeed.integration;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class StaticFilesIT {
@ParameterizedTest
@ValueSource(strings = { "/", "/openapi.json", "/openapi.yaml" })
void servedWithoutCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "no-cache");
}
@ParameterizedTest
@ValueSource(strings = { "/favicon.ico" })
void servedWithCache(String path) {
RestAssured.given().when().get(path).then().statusCode(200).header("Cache-Control", "public, immutable, max-age=86400");
}
}

View File

@@ -1,161 +1,161 @@
package com.commafeed.integration;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.Session;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import lombok.extern.slf4j.Slf4j;
@QuarkusTest
@Slf4j
class WebSocketIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void tearDown() {
RestAssured.reset();
}
@Test
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<CloseReason> closeReasonRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
connected.set(true);
}
@Override
public void onClose(Session session, CloseReason closeReason) {
closeReasonRef.set(closeReason);
}
}, buildConfig(List.of()), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null);
}
}
@Test
void subscribeAndGetsNotified() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
Long subscriptionId = subscribe(getFeedUrl());
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":2", messageRef.get());
}
}
@Test
void notNotifiedForFilteredEntries() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
FeedModificationRequest req = new FeedModificationRequest();
req.setId(subscriptionId);
req.setName("feed-name");
req.setFilter("!title.contains('item 4')");
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK);
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
feedNowReturnsMoreEntries();
forceRefreshAllFeeds();
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":1", messageRef.get());
}
}
@Test
void pingPong() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
session.getAsyncRemote().sendText("ping");
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("pong", messageRef.get());
}
}
private ClientEndpointConfig buildConfig(List<HttpCookie> cookies) {
return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
headers.put(HttpHeaders.COOKIE,
Collections.singletonList(cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))));
}
}).build();
}
}
package com.commafeed.integration;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.Session;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import lombok.extern.slf4j.Slf4j;
@QuarkusTest
@Slf4j
class WebSocketIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void tearDown() {
RestAssured.reset();
}
@Test
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<CloseReason> closeReasonRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
connected.set(true);
}
@Override
public void onClose(Session session, CloseReason closeReason) {
closeReasonRef.set(closeReason);
}
}, buildConfig(List.of()), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null);
}
}
@Test
void subscribeAndGetsNotified() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
Long subscriptionId = subscribe(getFeedUrl());
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":2", messageRef.get());
}
}
@Test
void notNotifiedForFilteredEntries() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
FeedModificationRequest req = new FeedModificationRequest();
req.setId(subscriptionId);
req.setName("feed-name");
req.setFilter("!title.contains('item 4')");
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/feed/modify").then().statusCode(HttpStatus.SC_OK);
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
feedNowReturnsMoreEntries();
forceRefreshAllFeeds();
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":1", messageRef.get());
}
}
@Test
void pingPong() throws DeploymentException, IOException {
List<HttpCookie> cookies = login();
AtomicBoolean connected = new AtomicBoolean();
AtomicReference<String> messageRef = new AtomicReference<>();
try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(String.class, messageRef::set);
connected.set(true);
}
}, buildConfig(cookies), URI.create(getWebSocketUrl()))) {
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
log.info("connected to {}", session.getRequestURI());
session.getAsyncRemote().sendText("ping");
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
Assertions.assertEquals("pong", messageRef.get());
}
}
private ClientEndpointConfig buildConfig(List<HttpCookie> cookies) {
return ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
headers.put(HttpHeaders.COOKIE,
Collections.singletonList(cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";"))));
}
}).build();
}
}

View File

@@ -1,102 +1,102 @@
package com.commafeed.integration.rest;
import java.util.List;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
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 com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class AdminIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Users {
@Test
void saveModifyAndDeleteNewUser() {
List<UserModel> existingUsers = getAllUsers();
createUser();
Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size());
modifyUser();
Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size());
deleteUser();
Assertions.assertEquals(existingUsers.size(), getAllUsers().size());
}
private void createUser() {
User user = new User();
user.setName("test");
user.setPassword("test".getBytes());
user.setEmail("test@test.com");
RestAssured.given()
.body(user)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/save")
.then()
.statusCode(HttpStatus.SC_OK);
}
private void modifyUser() {
List<UserModel> existingUsers = getAllUsers();
UserModel user = existingUsers.stream()
.filter(u -> u.getName().equals("test"))
.findFirst()
.orElseThrow(() -> new NullPointerException("User not found"));
user.setEmail("new-email@provider.com");
RestAssured.given()
.body(user)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/save")
.then()
.statusCode(HttpStatus.SC_OK);
}
private void deleteUser() {
List<UserModel> existingUsers = getAllUsers();
UserModel user = existingUsers.stream()
.filter(u -> u.getName().equals("test"))
.findFirst()
.orElseThrow(() -> new NullPointerException("User not found"));
IDRequest req = new IDRequest();
req.setId(user.getId());
RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/delete")
.then()
.statusCode(HttpStatus.SC_OK);
}
private List<UserModel> getAllUsers() {
return List.of(
RestAssured.given().get("rest/admin/user/getAll").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel[].class));
}
}
}
package com.commafeed.integration.rest;
import java.util.List;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
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 com.commafeed.backend.model.User;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class AdminIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Users {
@Test
void saveModifyAndDeleteNewUser() {
List<UserModel> existingUsers = getAllUsers();
createUser();
Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size());
modifyUser();
Assertions.assertEquals(existingUsers.size() + 1, getAllUsers().size());
deleteUser();
Assertions.assertEquals(existingUsers.size(), getAllUsers().size());
}
private void createUser() {
User user = new User();
user.setName("test");
user.setPassword("test".getBytes());
user.setEmail("test@test.com");
RestAssured.given()
.body(user)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/save")
.then()
.statusCode(HttpStatus.SC_OK);
}
private void modifyUser() {
List<UserModel> existingUsers = getAllUsers();
UserModel user = existingUsers.stream()
.filter(u -> u.getName().equals("test"))
.findFirst()
.orElseThrow(() -> new NullPointerException("User not found"));
user.setEmail("new-email@provider.com");
RestAssured.given()
.body(user)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/save")
.then()
.statusCode(HttpStatus.SC_OK);
}
private void deleteUser() {
List<UserModel> existingUsers = getAllUsers();
UserModel user = existingUsers.stream()
.filter(u -> u.getName().equals("test"))
.findFirst()
.orElseThrow(() -> new NullPointerException("User not found"));
IDRequest req = new IDRequest();
req.setId(user.getId());
RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/admin/user/delete")
.then()
.statusCode(HttpStatus.SC_OK);
}
private List<UserModel> getAllUsers() {
return List.of(
RestAssured.given().get("rest/admin/user/getAll").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel[].class));
}
}
}

View File

@@ -1,266 +1,266 @@
package com.commafeed.integration.rest;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Objects;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
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 com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.FeedInfo;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.FeedInfoRequest;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class FeedIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Fetch {
@Test
void fetchFeed() {
FeedInfoRequest req = new FeedInfoRequest();
req.setUrl(getFeedUrl());
FeedInfo feedInfo = RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/fetch")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(FeedInfo.class);
Assertions.assertEquals("CommaFeed test feed", feedInfo.getTitle());
Assertions.assertEquals(getFeedUrl(), feedInfo.getUrl());
}
}
@Nested
class Subscribe {
@Test
void subscribeAndReadEntries() {
long subscriptionId = subscribe(getFeedUrl());
Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2);
}
@Test
void subscribeFromUrl() {
RestAssured.given()
.queryParam("url", getFeedUrl())
.redirects()
.follow(false)
.get("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT);
}
@Test
void unsubscribeFromUnknownFeed() {
Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L));
}
@Test
void unsubscribeFromKnownFeed() {
long subscriptionId = subscribe(getFeedUrl());
Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId));
}
private int unsubsribe(long subscriptionId) {
IDRequest request = new IDRequest();
request.setId(subscriptionId);
return RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/unsubscribe")
.then()
.extract()
.statusCode();
}
}
@Nested
class Mark {
@Test
void markWithoutDates() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, null, null);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
@Test
void markOlderThan() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, LocalDate.of(2023, 12, 28).atStartOfDay().toInstant(ZoneOffset.UTC), null);
Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
}
@Test
void markInsertedBeforeBeforeSubscription() {
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead));
}
@Test
void markInsertedBeforeAfterSubscription() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().plus(Duration.ofSeconds(1));
markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
private void markFeedEntries(long subscriptionId, Instant olderThan, Instant insertedBefore) {
MarkRequest request = new MarkRequest();
request.setId(String.valueOf(subscriptionId));
request.setOlderThan(olderThan == null ? null : olderThan.toEpochMilli());
request.setInsertedBefore(insertedBefore == null ? null : insertedBefore.toEpochMilli());
RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/mark")
.then()
.statusCode(HttpStatus.SC_OK);
}
}
@Nested
class Refresh {
@Test
void refresh() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
IDRequest request = new IDRequest();
request.setId(subscriptionId);
RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/refresh")
.then()
.statusCode(HttpStatus.SC_OK);
Awaitility.await()
.atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
}
@Test
void refreshAll() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
Assertions.assertEquals(HttpStatus.SC_OK, forceRefreshAllFeeds());
Awaitility.await()
.atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
Assertions.assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, forceRefreshAllFeeds());
}
}
@Nested
class Modify {
@Test
void modify() {
Long subscriptionId = subscribe(getFeedUrl());
Subscription subscription = getSubscription(subscriptionId);
FeedModificationRequest req = new FeedModificationRequest();
req.setId(subscriptionId);
req.setName("new name");
req.setCategoryId(subscription.getCategoryId());
RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/modify")
.then()
.statusCode(HttpStatus.SC_OK);
subscription = getSubscription(subscriptionId);
Assertions.assertEquals("new name", subscription.getName());
}
}
@Nested
class Favicon {
@Test
void favicon() throws IOException {
Long subscriptionId = subscribe(getFeedUrl());
byte[] icon = RestAssured.given()
.get("rest/feed/favicon/{id}", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.header(HttpHeaders.CACHE_CONTROL, "max-age=2592000")
.extract()
.response()
.asByteArray();
byte[] defaultFavicon = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif")));
Assertions.assertArrayEquals(defaultFavicon, icon);
}
}
@Nested
class Opml {
@Test
void importExportOpml() {
importOpml();
String opml = RestAssured.given().get("rest/feed/export").then().statusCode(HttpStatus.SC_OK).extract().asString();
Assertions.assertTrue(opml.contains("<title>admin subscriptions in CommaFeed</title>"));
}
void importOpml() {
InputStream stream = Objects.requireNonNull(getClass().getResourceAsStream("/opml/opml_v2.0.xml"));
RestAssured.given()
.multiPart("file", "opml_v2.0.xml", stream, MediaType.MULTIPART_FORM_DATA)
.post("rest/feed/import")
.then()
.statusCode(HttpStatus.SC_OK);
}
}
}
package com.commafeed.integration.rest;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Objects;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.apache.commons.io.IOUtils;
import org.apache.hc.core5.http.HttpStatus;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
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 com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.FeedInfo;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.FeedInfoRequest;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class FeedIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Nested
class Fetch {
@Test
void fetchFeed() {
FeedInfoRequest req = new FeedInfoRequest();
req.setUrl(getFeedUrl());
FeedInfo feedInfo = RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/fetch")
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(FeedInfo.class);
Assertions.assertEquals("CommaFeed test feed", feedInfo.getTitle());
Assertions.assertEquals(getFeedUrl(), feedInfo.getUrl());
}
}
@Nested
class Subscribe {
@Test
void subscribeAndReadEntries() {
long subscriptionId = subscribe(getFeedUrl());
Awaitility.await().atMost(Duration.ofSeconds(15)).until(() -> getFeedEntries(subscriptionId), e -> e.getEntries().size() == 2);
}
@Test
void subscribeFromUrl() {
RestAssured.given()
.queryParam("url", getFeedUrl())
.redirects()
.follow(false)
.get("rest/feed/subscribe")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT);
}
@Test
void unsubscribeFromUnknownFeed() {
Assertions.assertEquals(HttpStatus.SC_NOT_FOUND, unsubsribe(1L));
}
@Test
void unsubscribeFromKnownFeed() {
long subscriptionId = subscribe(getFeedUrl());
Assertions.assertEquals(HttpStatus.SC_OK, unsubsribe(subscriptionId));
}
private int unsubsribe(long subscriptionId) {
IDRequest request = new IDRequest();
request.setId(subscriptionId);
return RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/unsubscribe")
.then()
.extract()
.statusCode();
}
}
@Nested
class Mark {
@Test
void markWithoutDates() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, null, null);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
@Test
void markOlderThan() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, LocalDate.of(2023, 12, 28).atStartOfDay().toInstant(ZoneOffset.UTC), null);
Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
}
@Test
void markInsertedBeforeBeforeSubscription() {
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().noneMatch(Entry::isRead));
}
@Test
void markInsertedBeforeAfterSubscription() {
long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().plus(Duration.ofSeconds(1));
markFeedEntries(subscriptionId, null, threshold);
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
private void markFeedEntries(long subscriptionId, Instant olderThan, Instant insertedBefore) {
MarkRequest request = new MarkRequest();
request.setId(String.valueOf(subscriptionId));
request.setOlderThan(olderThan == null ? null : olderThan.toEpochMilli());
request.setInsertedBefore(insertedBefore == null ? null : insertedBefore.toEpochMilli());
RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/mark")
.then()
.statusCode(HttpStatus.SC_OK);
}
}
@Nested
class Refresh {
@Test
void refresh() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
IDRequest request = new IDRequest();
request.setId(subscriptionId);
RestAssured.given()
.body(request)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/refresh")
.then()
.statusCode(HttpStatus.SC_OK);
Awaitility.await()
.atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
}
@Test
void refreshAll() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
// mariadb/mysql timestamp precision is 1 second
Instant threshold = Instant.now().minus(Duration.ofSeconds(1));
Assertions.assertEquals(HttpStatus.SC_OK, forceRefreshAllFeeds());
Awaitility.await()
.atMost(Duration.ofSeconds(15))
.until(() -> getSubscription(subscriptionId), f -> f.getLastRefresh().isAfter(threshold));
Assertions.assertEquals(HttpStatus.SC_TOO_MANY_REQUESTS, forceRefreshAllFeeds());
}
}
@Nested
class Modify {
@Test
void modify() {
Long subscriptionId = subscribe(getFeedUrl());
Subscription subscription = getSubscription(subscriptionId);
FeedModificationRequest req = new FeedModificationRequest();
req.setId(subscriptionId);
req.setName("new name");
req.setCategoryId(subscription.getCategoryId());
RestAssured.given()
.body(req)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/feed/modify")
.then()
.statusCode(HttpStatus.SC_OK);
subscription = getSubscription(subscriptionId);
Assertions.assertEquals("new name", subscription.getName());
}
}
@Nested
class Favicon {
@Test
void favicon() throws IOException {
Long subscriptionId = subscribe(getFeedUrl());
byte[] icon = RestAssured.given()
.get("rest/feed/favicon/{id}", subscriptionId)
.then()
.statusCode(HttpStatus.SC_OK)
.header(HttpHeaders.CACHE_CONTROL, "max-age=2592000")
.extract()
.response()
.asByteArray();
byte[] defaultFavicon = IOUtils.toByteArray(Objects.requireNonNull(getClass().getResource("/images/default_favicon.gif")));
Assertions.assertArrayEquals(defaultFavicon, icon);
}
}
@Nested
class Opml {
@Test
void importExportOpml() {
importOpml();
String opml = RestAssured.given().get("rest/feed/export").then().statusCode(HttpStatus.SC_OK).extract().asString();
Assertions.assertTrue(opml.contains("<title>admin subscriptions in CommaFeed</title>"));
}
void importOpml() {
InputStream stream = Objects.requireNonNull(getClass().getResourceAsStream("/opml/opml_v2.0.xml"));
RestAssured.given()
.multiPart("file", "opml_v2.0.xml", stream, MediaType.MULTIPART_FORM_DATA)
.post("rest/feed/import")
.then()
.statusCode(HttpStatus.SC_OK);
}
}
}

View File

@@ -1,89 +1,89 @@
package com.commafeed.integration.rest;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.backend.Digests;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.resource.fever.FeverResponse;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class FeverIT extends BaseIT {
private Long userId;
private String apiKey;
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword("admin");
req.setNewApiKey(true);
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/profile").then().statusCode(HttpStatus.SC_OK);
// retrieve api key
UserModel user = RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel.class);
this.apiKey = user.getApiKey();
this.userId = user.getId();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void invalidApiKey() {
FeverResponse response = fetch("feeds", "invalid-key");
Assertions.assertFalse(response.isAuth());
}
@Test
void validApiKey() {
FeverResponse response = fetch("feeds", apiKey);
Assertions.assertTrue(response.isAuth());
}
@Test
void feeds() {
subscribe(getFeedUrl());
FeverResponse feverResponse = fetch("feeds");
Assertions.assertEquals(1, feverResponse.getFeeds().size());
}
@Test
void unreadEntries() {
subscribeAndWaitForEntries(getFeedUrl());
FeverResponse feverResponse = fetch("unread_item_ids");
Assertions.assertEquals(2, feverResponse.getUnreadItemIds().size());
}
private FeverResponse fetch(String what) {
return fetch(what, apiKey);
}
private FeverResponse fetch(String what, String apiKey) {
return RestAssured.given()
.auth()
.none()
.formParam("api_key", Digests.md5Hex("admin:" + apiKey))
.formParam(what, 1)
.post("rest/fever/user/{userId}", userId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(FeverResponse.class);
}
}
package com.commafeed.integration.rest;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.backend.Digests;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.resource.fever.FeverResponse;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class FeverIT extends BaseIT {
private Long userId;
private String apiKey;
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
// create api key
ProfileModificationRequest req = new ProfileModificationRequest();
req.setCurrentPassword("admin");
req.setNewApiKey(true);
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/profile").then().statusCode(HttpStatus.SC_OK);
// retrieve api key
UserModel user = RestAssured.given().get("rest/user/profile").then().statusCode(HttpStatus.SC_OK).extract().as(UserModel.class);
this.apiKey = user.getApiKey();
this.userId = user.getId();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void invalidApiKey() {
FeverResponse response = fetch("feeds", "invalid-key");
Assertions.assertFalse(response.isAuth());
}
@Test
void validApiKey() {
FeverResponse response = fetch("feeds", apiKey);
Assertions.assertTrue(response.isAuth());
}
@Test
void feeds() {
subscribe(getFeedUrl());
FeverResponse feverResponse = fetch("feeds");
Assertions.assertEquals(1, feverResponse.getFeeds().size());
}
@Test
void unreadEntries() {
subscribeAndWaitForEntries(getFeedUrl());
FeverResponse feverResponse = fetch("unread_item_ids");
Assertions.assertEquals(2, feverResponse.getUnreadItemIds().size());
}
private FeverResponse fetch(String what) {
return fetch(what, apiKey);
}
private FeverResponse fetch(String what, String apiKey) {
return RestAssured.given()
.auth()
.none()
.formParam("api_key", Digests.md5Hex("admin:" + apiKey))
.formParam(what, 1)
.post("rest/fever/user/{userId}", userId)
.then()
.statusCode(HttpStatus.SC_OK)
.extract()
.as(FeverResponse.class);
}
}

View File

@@ -1,27 +1,27 @@
package com.commafeed.integration.rest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.ServerInfo;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class ServerIT extends BaseIT {
@Test
void getServerInfos() {
ServerInfo serverInfos = RestAssured.given().get("/rest/server/get").then().statusCode(200).extract().as(ServerInfo.class);
Assertions.assertTrue(serverInfos.isAllowRegistrations());
Assertions.assertTrue(serverInfos.isSmtpEnabled());
Assertions.assertTrue(serverInfos.isDemoAccountEnabled());
Assertions.assertTrue(serverInfos.isWebsocketEnabled());
Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval());
Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration());
}
}
package com.commafeed.integration.rest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.ServerInfo;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class ServerIT extends BaseIT {
@Test
void getServerInfos() {
ServerInfo serverInfos = RestAssured.given().get("/rest/server/get").then().statusCode(200).extract().as(ServerInfo.class);
Assertions.assertTrue(serverInfos.isAllowRegistrations());
Assertions.assertTrue(serverInfos.isSmtpEnabled());
Assertions.assertTrue(serverInfos.isDemoAccountEnabled());
Assertions.assertTrue(serverInfos.isWebsocketEnabled());
Assertions.assertEquals(900000, serverInfos.getWebsocketPingInterval());
Assertions.assertEquals(30000, serverInfos.getTreeReloadInterval());
Assertions.assertEquals(60000, serverInfos.getForceRefreshCooldownDuration());
}
}

View File

@@ -1,53 +1,53 @@
package com.commafeed.integration.rest;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.request.PasswordResetRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.vertx.ext.mail.MailMessage;
@QuarkusTest
class UserIT extends BaseIT {
@Inject
MockMailbox mailbox;
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
mailbox.clear();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void resetPassword() {
PasswordResetRequest req = new PasswordResetRequest();
req.setEmail("admin@commafeed.com");
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/passwordReset").then().statusCode(200);
List<MailMessage> mails = mailbox.getMailMessagesSentTo("admin@commafeed.com");
Assertions.assertEquals(1, mails.size());
MailMessage message = mails.get(0);
Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject());
Assertions.assertTrue(message.getHtml().startsWith("You asked for password recovery for account 'admin'"));
Assertions.assertEquals("admin@commafeed.com", message.getTo().get(0));
}
}
package com.commafeed.integration.rest;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.request.PasswordResetRequest;
import com.commafeed.integration.BaseIT;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.vertx.ext.mail.MailMessage;
@QuarkusTest
class UserIT extends BaseIT {
@Inject
MockMailbox mailbox;
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
mailbox.clear();
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void resetPassword() {
PasswordResetRequest req = new PasswordResetRequest();
req.setEmail("admin@commafeed.com");
RestAssured.given().body(req).contentType(MediaType.APPLICATION_JSON).post("rest/user/passwordReset").then().statusCode(200);
List<MailMessage> mails = mailbox.getMailMessagesSentTo("admin@commafeed.com");
Assertions.assertEquals(1, mails.size());
MailMessage message = mails.get(0);
Assertions.assertEquals("CommaFeed - Password recovery", message.getSubject());
Assertions.assertTrue(message.getHtml().startsWith("You asked for password recovery for account 'admin'"));
Assertions.assertEquals("admin@commafeed.com", message.getTo().get(0));
}
}

View File

@@ -1,49 +1,49 @@
package com.commafeed.integration.servlet;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.Settings;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class CustomCodeIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
// get settings
Settings settings = RestAssured.given().get("rest/user/settings").then().statusCode(200).extract().as(Settings.class);
// update settings
settings.setCustomJs("custom-js");
settings.setCustomCss("custom-css");
RestAssured.given()
.body(settings)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/user/settings")
.then()
.statusCode(HttpStatus.SC_OK);
// check custom code servlets
RestAssured.given().get("custom_js.js").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-js"));
RestAssured.given().get("custom_css.css").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-css"));
}
}
package com.commafeed.integration.servlet;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.core5.http.HttpStatus;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.frontend.model.Settings;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class CustomCodeIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
// get settings
Settings settings = RestAssured.given().get("rest/user/settings").then().statusCode(200).extract().as(Settings.class);
// update settings
settings.setCustomJs("custom-js");
settings.setCustomCss("custom-css");
RestAssured.given()
.body(settings)
.contentType(MediaType.APPLICATION_JSON)
.post("rest/user/settings")
.then()
.statusCode(HttpStatus.SC_OK);
// check custom code servlets
RestAssured.given().get("custom_js.js").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-js"));
RestAssured.given().get("custom_css.css").then().statusCode(HttpStatus.SC_OK).body(CoreMatchers.is("custom-css"));
}
}

View File

@@ -1,38 +1,38 @@
package com.commafeed.integration.servlet;
import java.net.HttpCookie;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.Headers;
@QuarkusTest
class LogoutIT extends BaseIT {
@Test
void test() {
List<HttpCookie> cookies = login();
Headers responseHeaders = RestAssured.given()
.header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.redirects()
.follow(false)
.get("logout")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT)
.extract()
.headers();
List<String> setCookieHeaders = responseHeaders.getValues(HttpHeaders.SET_COOKIE);
Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0));
}
}
package com.commafeed.integration.servlet;
import java.net.HttpCookie;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.Headers;
@QuarkusTest
class LogoutIT extends BaseIT {
@Test
void test() {
List<HttpCookie> cookies = login();
Headers responseHeaders = RestAssured.given()
.header(HttpHeaders.COOKIE, cookies.stream().map(HttpCookie::toString).collect(Collectors.joining(";")))
.redirects()
.follow(false)
.get("logout")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT)
.extract()
.headers();
List<String> setCookieHeaders = responseHeaders.getValues(HttpHeaders.SET_COOKIE);
Assertions.assertTrue(setCookieHeaders.stream().flatMap(c -> HttpCookie.parse(c).stream()).allMatch(c -> c.getMaxAge() == 0));
}
}

View File

@@ -1,41 +1,41 @@
package com.commafeed.integration.servlet;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class NextUnreadIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
subscribeAndWaitForEntries(getFeedUrl());
RestAssured.given()
.redirects()
.follow(false)
.get("next")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "https://hostname.local/commafeed/2");
}
}
package com.commafeed.integration.servlet;
import jakarta.ws.rs.core.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class NextUnreadIT extends BaseIT {
@BeforeEach
void setup() {
RestAssured.authentication = RestAssured.preemptive().basic("admin", "admin");
}
@AfterEach
void cleanup() {
RestAssured.reset();
}
@Test
void test() {
subscribeAndWaitForEntries(getFeedUrl());
RestAssured.given()
.redirects()
.follow(false)
.get("next")
.then()
.statusCode(HttpStatus.SC_TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "https://hostname.local/commafeed/2");
}
}

View File

@@ -1,17 +1,17 @@
package com.commafeed.integration.servlet;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class RobotsTxtIT extends BaseIT {
@Test
void test() {
RestAssured.given().get("robots.txt").then().statusCode(200).body(CoreMatchers.is("User-agent: *\nDisallow: /"));
}
}
package com.commafeed.integration.servlet;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class RobotsTxtIT extends BaseIT {
@Test
void test() {
RestAssured.given().get("robots.txt").then().statusCode(200).body(CoreMatchers.is("User-agent: *\nDisallow: /"));
}
}