add more integration tests

This commit is contained in:
Athou
2025-07-21 19:39:13 +02:00
parent 23a91aab12
commit 865c80f87b
2 changed files with 271 additions and 25 deletions

View File

@@ -0,0 +1,153 @@
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;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryContentDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
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;
@ExtendWith(MockitoExtension.class)
class DatabaseCleaningServiceTest {
private static final int BATCH_SIZE = 100;
@Mock
private CommaFeedConfiguration config;
@Mock
private CommaFeedConfiguration.Database databaseConfig;
@Mock
private CommaFeedConfiguration.Database.Cleanup cleaningConfig;
@Mock
private UnitOfWork unitOfWork;
@Mock
private FeedDAO feedDAO;
@Mock
private FeedEntryDAO feedEntryDAO;
@Mock
private FeedEntryContentDAO feedEntryContentDAO;
@Mock
private FeedEntryStatusDAO feedEntryStatusDAO;
@Mock
private MetricRegistry metrics;
@Mock
private Meter entriesDeletedMeter;
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);
Mockito.when(unitOfWork.call(Mockito.any())).thenAnswer(invocation -> ((Callable<?>) invocation.getArgument(0)).call());
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);
// 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);
service.cleanFeedsWithoutSubscriptions();
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);
service.cleanContentsWithoutEntries();
Mockito.verify(feedEntryContentDAO, Mockito.times(3)).deleteWithoutEntries(Mockito.anyInt());
}
@Test
void cleanEntriesForFeedsExceedingCapacityDeletesOldEntries() {
FeedCapacity feed1 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed1.getId()).thenReturn(1L);
Mockito.when(feed1.getCapacity()).thenReturn(180L);
FeedCapacity feed2 = Mockito.mock(FeedCapacity.class);
Mockito.when(feed2.getId()).thenReturn(2L);
Mockito.when(feed2.getCapacity()).thenReturn(120L);
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE))
.thenReturn(Arrays.asList(feed1, feed2))
.thenReturn(Collections.emptyList());
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100)).thenReturn(80);
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50)).thenReturn(50);
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70)).thenReturn(70);
service.cleanEntriesForFeedsExceedingCapacity(50);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
@Test
void cleanEntriesOlderThanDeletesOldEntries() {
Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC);
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE)).thenReturn(100, 50, 0);
service.cleanEntriesOlderThan(cutoff);
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE);
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
}
@Test
void cleanStatusesOlderThanDeletesOldStatuses() {
Instant cutoff = LocalDate.now().minusDays(60).atStartOfDay().toInstant(ZoneOffset.UTC);
Mockito.when(feedEntryStatusDAO.deleteOldStatuses(cutoff, BATCH_SIZE)).thenReturn(200L, 100L, 0L);
service.cleanStatusesOlderThan(cutoff);
Mockito.verify(feedEntryStatusDAO, Mockito.times(3)).deleteOldStatuses(cutoff, BATCH_SIZE);
}
}

View File

@@ -7,20 +7,24 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.commafeed.backend.Digests;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.StarRequest;
import com.commafeed.frontend.resource.fever.FeverResponse;
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
import com.commafeed.integration.BaseIT;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import lombok.Setter;
@QuarkusTest
class FeverIT extends BaseIT {
private Long userId;
private String apiKey;
private FeverClient client;
@BeforeEach
void setup() {
@@ -34,8 +38,7 @@ class FeverIT extends BaseIT {
// 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();
this.client = new FeverClient(user.getId(), user.getApiKey());
}
@AfterEach
@@ -45,65 +48,155 @@ class FeverIT extends BaseIT {
@Test
void invalidApiKey() {
FeverResponse response = fetch("feeds", "invalid-key");
client.apiKey = "invalid-key";
FeverResponse response = client.execute("feeds");
Assertions.assertFalse(response.isAuth());
}
@Test
void validApiKey() {
FeverResponse response = fetch("feeds", apiKey);
FeverResponse response = client.execute("feeds");
Assertions.assertTrue(response.isAuth());
}
@Test
void feeds() {
subscribe(getFeedUrl());
FeverResponse feverResponse = fetch("feeds");
FeverResponse feverResponse = client.execute("feeds");
Assertions.assertEquals(1, feverResponse.getFeeds().size());
}
@Test
void unreadEntries() {
subscribeAndWaitForEntries(getFeedUrl());
FeverResponse feverResponse = fetch("unread_item_ids");
FeverResponse feverResponse = client.execute("unread_item_ids");
Assertions.assertEquals(2, feverResponse.getUnreadItemIds().size());
}
@Test
void entries() {
subscribeAndWaitForEntries(getFeedUrl());
FeverResponse feverResponse = fetch("items");
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
FeverResponse feverResponse = client.execute("items");
Assertions.assertEquals(2, feverResponse.getItems().size());
FeverItem item = feverResponse.getItems().get(0);
Assertions.assertEquals(subscriptionId, item.getFeedId());
Assertions.assertEquals("Item 2", item.getTitle());
Assertions.assertEquals("Item 2 description", item.getHtml());
Assertions.assertEquals("https://hostname.local/commafeed/2", item.getUrl());
Assertions.assertFalse(item.isSaved());
Assertions.assertFalse(item.isRead());
}
@Test
void entriesByIds() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().get(0);
FeverResponse feverResponse = client.execute("items", new Param("with_ids", entry.getId()));
Assertions.assertEquals(1, feverResponse.getItems().size());
}
@Test
void savedEntries() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().get(0);
StarRequest starRequest = new StarRequest();
starRequest.setId(entry.getId());
starRequest.setFeedId(subscriptionId);
starRequest.setStarred(true);
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star");
FeverResponse feverResponse = client.execute("saved_item_ids");
Assertions.assertEquals(1, feverResponse.getSavedItemIds().size());
}
@Test
void markEntry() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().get(0);
client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "read"));
Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "unread"));
Assertions.assertEquals(0, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isRead).count());
}
@Test
void markFeed() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
client.execute("_", new Param("mark", "feed"), new Param("id", String.valueOf(subscriptionId)), new Param("as", "read"));
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
@Test
void markCategory() {
String categoryId = createCategory("test-category");
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl(), categoryId);
client.execute("_", new Param("mark", "group"), new Param("id", String.valueOf(categoryId)), new Param("as", "read"));
Assertions.assertTrue(getFeedEntries(subscriptionId).getEntries().stream().allMatch(Entry::isRead));
}
@Test
void tagEntry() {
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
Entry entry = getFeedEntries(subscriptionId).getEntries().get(0);
client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "saved"));
Assertions.assertEquals(1, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isStarred).count());
client.execute("_", new Param("mark", "item"), new Param("id", entry.getId()), new Param("as", "unsaved"));
Assertions.assertEquals(0, getFeedEntries(subscriptionId).getEntries().stream().filter(Entry::isStarred).count());
}
@Test
void groups() {
createCategory("category-1");
FeverResponse feverResponse = fetch("groups");
FeverResponse feverResponse = client.execute("groups");
Assertions.assertEquals(1, feverResponse.getGroups().size());
Assertions.assertEquals("category-1", feverResponse.getGroups().get(0).getTitle());
}
@Test
void links() {
FeverResponse feverResponse = fetch("links");
FeverResponse feverResponse = client.execute("links");
Assertions.assertTrue(feverResponse.getLinks().isEmpty());
}
private FeverResponse fetch(String what) {
return fetch(what, apiKey);
private static class FeverClient {
private final Long userId;
@Setter
private String apiKey;
public FeverClient(Long userId, String apiKey) {
this.userId = userId;
this.apiKey = apiKey;
}
private FeverResponse execute(String action, Param... params) {
RequestSpecification spec = RestAssured.given()
.auth()
.none()
.formParam("api_key", Digests.md5Hex("admin:" + apiKey))
.formParam(action, 1);
for (Param param : params) {
spec.formParam(param.name(), param.value());
}
return spec.post("rest/fever/user/{userId}", userId).then().statusCode(HttpStatus.SC_OK).extract().as(FeverResponse.class);
}
}
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);
private record Param(String name, String value) {
}
}