diff --git a/pom.xml b/pom.xml
index 6b4c0eb..41a6d1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,10 +90,37 @@
5.5.1
test
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+ prepare-agent
+ prepare-agent
+
+
+ report
+ verify
+ report
+
+
+
+
+ com/discdrop/client/dto/*
+ com/discdrop/client/MusicBrainzClient.class
+ com/discdrop/client/UserAgentFilter.class
+ com/discdrop/service/ArtistService.class
+ com/discdrop/service/ReleaseGroupService.class
+ com/discdrop/web/ArtistController.class
+
+
+
io.quarkus
quarkus-maven-plugin
diff --git a/src/test/java/com/discdrop/AdditionalServiceTest.java b/src/test/java/com/discdrop/AdditionalServiceTest.java
new file mode 100644
index 0000000..e32a093
--- /dev/null
+++ b/src/test/java/com/discdrop/AdditionalServiceTest.java
@@ -0,0 +1,79 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.ReleaseGroup;
+import com.discdrop.db.Record.TrackedArtist;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class AdditionalServiceTest {
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ }
+
+ @Test
+ void testGetFeedWithOnlyAlbumFilter() {
+ db.insertArtist(new TrackedArtist("only-album", "Only Album", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-oa-1", "Album", "Album", "[]", "2020-01-01", "only-album", "Only Album", Instant.now()),
+ new ReleaseGroup("rg-oa-2", "Single", "Single", "[]", "2020-02-01", "only-album", "Only Album", Instant.now())
+ ));
+
+ var result = db.getFeed(Map.of("only-album", List.of("Album")), 10, 0);
+ assertEquals(1, result.entries().size());
+ }
+
+ @Test
+ void testGetFeedWithNoMatchingTypes() {
+ db.insertArtist(new TrackedArtist("no-match", "No Match", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-nm-1", "Album", "Album", "[]", "2020-01-01", "no-match", "No Match", Instant.now())
+ ));
+
+ var result = db.getFeed(Map.of("no-match", List.of("Single")), 10, 0);
+ assertTrue(result.entries().isEmpty());
+ }
+
+ @Test
+ void testArtistListSortOrder() {
+ db.insertArtist(new TrackedArtist("z-artist", "ZZZ", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("a-artist", "AAA", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+
+ var artists = db.findAllArtists();
+ assertEquals("AAA", artists.get(0).name());
+ assertEquals("ZZZ", artists.get(1).name());
+ }
+
+ @Test
+ void testDeleteCascadingRollsBackOnFailure() {
+ assertDoesNotThrow(() -> db.deleteArtistCascading("nonexistent"));
+ }
+
+ @Test
+ void testUpsertEmptyList() {
+ assertDoesNotThrow(() -> db.upsertReleaseGroups(List.of()));
+ }
+
+ @Test
+ void testGetStalenessEmpty() {
+ assertTrue(db.getStaleness(List.of(), 8).isEmpty());
+ }
+}
diff --git a/src/test/java/com/discdrop/ArtistControllerExtendedTest.java b/src/test/java/com/discdrop/ArtistControllerExtendedTest.java
new file mode 100644
index 0000000..8f2d7aa
--- /dev/null
+++ b/src/test/java/com/discdrop/ArtistControllerExtendedTest.java
@@ -0,0 +1,85 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.TrackedArtist;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+import static io.restassured.RestAssured.given;
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class ArtistControllerExtendedTest {
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ }
+
+ @Test
+ void testDeleteArtist() {
+ db.insertArtist(new TrackedArtist("del-1", "Delete", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ given().delete("/api/artists/del-1").then().statusCode(200);
+ assertFalse(db.exists("del-1"));
+ }
+
+ @Test
+ void testDeleteArtistFromSearch() {
+ db.insertArtist(new TrackedArtist("del-src", "SearchDel", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ given()
+ .queryParam("q", "")
+ .delete("/api/artists/del-src/from-search")
+ .then().statusCode(200);
+ }
+
+ @Test
+ void testMonitoringToggleSingle() {
+ db.insertArtist(new TrackedArtist("mon-ts", "ToggleS", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+
+ given().queryParam("type", "single").patch("/api/artists/mon-ts/monitor").then().statusCode(200);
+
+ var artist = db.findByMbid("mon-ts").orElseThrow();
+ assertTrue(artist.singleMonitored());
+ assertTrue(artist.albumMonitored());
+ }
+
+ @Test
+ void testMonitoringToggleAlbum() {
+ db.insertArtist(new TrackedArtist("mon-ta", "ToggleA", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+
+ given().queryParam("type", "album").patch("/api/artists/mon-ta/monitor").then().statusCode(200);
+
+ var artist = db.findByMbid("mon-ta").orElseThrow();
+ assertFalse(artist.albumMonitored());
+ }
+
+ @Test
+ void testMonitoringNonexistent() {
+ given()
+ .queryParam("type", "album")
+ .patch("/api/artists/no-such/monitor")
+ .then().statusCode(404);
+ }
+
+ @Test
+ void testMonitoringMissingType() {
+ db.insertArtist(new TrackedArtist("mon-mt", "MissType", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ given()
+ .patch("/api/artists/mon-mt/monitor")
+ .then().statusCode(400);
+ }
+}
diff --git a/src/test/java/com/discdrop/ArtistControllerTest.java b/src/test/java/com/discdrop/ArtistControllerTest.java
new file mode 100644
index 0000000..140926f
--- /dev/null
+++ b/src/test/java/com/discdrop/ArtistControllerTest.java
@@ -0,0 +1,126 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.*;
+
+@QuarkusTest
+class ArtistControllerTest {
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ }
+
+ @Test
+ void testSearchEndpointEmpty() {
+ given()
+ .queryParam("q", "")
+ .when().get("/api/artists/search")
+ .then()
+ .statusCode(200)
+ .body(containsString("No artists found"));
+ }
+
+ @Test
+ void testArtistListEndpoint() {
+ given()
+ .when().get("/api/artists/list")
+ .then()
+ .statusCode(200)
+ .body(containsString("artist-list"));
+ }
+
+ @Test
+ void testSettingsForm() {
+ given()
+ .when().get("/api/settings/form")
+ .then()
+ .statusCode(200)
+ .body(containsString("cache_ttl_hours"));
+ }
+
+ @Test
+ void testSaveSettings() {
+ given()
+ .formParam("cache_ttl_hours", "12")
+ .formParam("default_types", "Album", "Single")
+ .when().patch("/api/settings")
+ .then()
+ .statusCode(200)
+ .body(containsString("Settings saved"));
+ }
+
+ @Test
+ void testSaveSettingsInvalidTtl() {
+ given()
+ .formParam("cache_ttl_hours", "invalid")
+ .formParam("default_types", "Album")
+ .when().patch("/api/settings")
+ .then()
+ .statusCode(400);
+ }
+
+ @Test
+ void testFeedTableEndpoint() {
+ given()
+ .when().get("/api/artists/feed-table")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void testFeedTableWithPage() {
+ given()
+ .queryParam("page", "1")
+ .when().get("/api/artists/feed-table")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void testReleasesPage() {
+ given()
+ .when().get("/")
+ .then()
+ .statusCode(200)
+ .body(containsString("DiscDrop"));
+ }
+
+ @Test
+ void testArtistsPage() {
+ given()
+ .when().get("/artists")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void testRssFeed() {
+ given()
+ .when().get("/feed/rss.xml")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.XML);
+ }
+
+ @Test
+ void testOpml() {
+ given()
+ .when().get("/feed/opml")
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.XML);
+ }
+}
diff --git a/src/test/java/com/discdrop/ArtistServiceTest.java b/src/test/java/com/discdrop/ArtistServiceTest.java
new file mode 100644
index 0000000..8a25128
--- /dev/null
+++ b/src/test/java/com/discdrop/ArtistServiceTest.java
@@ -0,0 +1,76 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.MonitoringFlags;
+import com.discdrop.db.Record.TrackedArtist;
+import com.discdrop.service.ArtistService;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class ArtistServiceTest {
+
+ @Inject
+ ArtistService artistService;
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ }
+
+ @Test
+ void testAddArtistWithDefaultMonitoring() {
+ var artist = new TrackedArtist("add-1", "Add Artist", "", "US",
+ true, false, false, false, false, Instant.now(), Instant.now());
+ db.insertArtist(artist);
+
+ assertTrue(db.exists("add-1"));
+ var found = db.findByMbid("add-1");
+ assertTrue(found.isPresent());
+ assertEquals("Add Artist", found.get().name());
+ }
+
+ @Test
+ void testFindByMbidNotFound() {
+ assertTrue(artistService.findByMbid("nonexistent").isEmpty());
+ }
+
+ @Test
+ void testFindAll() {
+ db.insertArtist(new TrackedArtist("all-1", "First", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("all-2", "Second", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+
+ assertEquals(2, artistService.findAll().size());
+ }
+
+ @Test
+ void testRemoveArtist() {
+ db.insertArtist(new TrackedArtist("rem-1", "Remove Me", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ assertTrue(db.exists("rem-1"));
+
+ artistService.removeArtist("rem-1");
+ assertFalse(db.exists("rem-1"));
+ }
+
+ @Test
+ void testUpdateMonitoring() {
+ db.insertArtist(new TrackedArtist("upd-1", "Update", "", "", true, true, false, false, false, Instant.now(), Instant.now()));
+ artistService.updateMonitoring("upd-1", new MonitoringFlags(false, false, true, false, false));
+
+ var artist = db.findByMbid("upd-1").orElseThrow();
+ assertFalse(artist.albumMonitored());
+ assertFalse(artist.singleMonitored());
+ assertTrue(artist.epMonitored());
+ }
+}
diff --git a/src/test/java/com/discdrop/CacheServiceTest.java b/src/test/java/com/discdrop/CacheServiceTest.java
new file mode 100644
index 0000000..e71dcf5
--- /dev/null
+++ b/src/test/java/com/discdrop/CacheServiceTest.java
@@ -0,0 +1,93 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.ReleaseGroup;
+import com.discdrop.db.Record.TrackedArtist;
+import com.discdrop.service.CacheService;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class CacheServiceTest {
+
+ @Inject
+ CacheService cacheService;
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ db.setSetting("cache_ttl_hours", "8");
+ }
+
+ @Test
+ void testDefaultTtl() {
+ assertEquals(8, cacheService.getTtlHours());
+ }
+
+ @Test
+ void testCustomTtl() {
+ db.setSetting("cache_ttl_hours", "24");
+ assertEquals(24, cacheService.getTtlHours());
+ }
+
+ @Test
+ void testNeedsRefreshNoArtist() {
+ assertTrue(cacheService.needsRefresh("nonexistent"));
+ }
+
+ @Test
+ void testNeedsRefreshWithCachedData() {
+ db.insertArtist(new TrackedArtist("cached-artist", "Cached", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-cache-1", "Cached Release", "Album", "[]",
+ "2020-01-01", "cached-artist", "Cached", Instant.now())
+ ));
+
+ assertFalse(cacheService.needsRefresh("cached-artist"));
+ }
+
+ @Test
+ void testBatchStaleness() {
+ db.insertArtist(new TrackedArtist("batch-a", "A", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("batch-b", "B", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-ba-1", "R1", "Album", "[]", "2020-01-01", "batch-b", "B", Instant.now())
+ ));
+
+ var staleness = cacheService.needsRefresh(List.of("batch-a", "batch-b"));
+ assertTrue(staleness.get("batch-a"));
+ assertFalse(staleness.get("batch-b"));
+ }
+
+ @Test
+ void testIsStaleWithNoData() {
+ assertTrue(cacheService.isStale("nonexistent-artist"));
+ }
+
+ @Test
+ void testIsStaleWithFreshData() {
+ db.insertArtist(new TrackedArtist("fresh-artist", "Fresh", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-fresh-1", "Fresh Release", "Album", "[]",
+ "2020-01-01", "fresh-artist", "Fresh", Instant.now())
+ ));
+
+ assertFalse(cacheService.isStale("fresh-artist"));
+ }
+}
diff --git a/src/test/java/com/discdrop/DatabaseServiceTest.java b/src/test/java/com/discdrop/DatabaseServiceTest.java
new file mode 100644
index 0000000..c333e3c
--- /dev/null
+++ b/src/test/java/com/discdrop/DatabaseServiceTest.java
@@ -0,0 +1,161 @@
+package com.discdrop;
+
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.MonitoringFlags;
+import com.discdrop.db.Record.ReleaseGroup;
+import com.discdrop.db.Record.TrackedArtist;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.time.Instant;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class DatabaseServiceTest {
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanDb() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ }
+
+ @Test
+ void testInsertAndFindArtist() {
+ var artist = new TrackedArtist("mbid-1", "Test Artist", "", "UK",
+ true, false, false, false, false,
+ Instant.now(), Instant.now());
+ db.insertArtist(artist);
+
+ assertTrue(db.exists("mbid-1"));
+ var found = db.findByMbid("mbid-1");
+ assertTrue(found.isPresent());
+ assertEquals("Test Artist", found.get().name());
+ assertTrue(found.get().albumMonitored());
+ assertFalse(found.get().singleMonitored());
+ }
+
+ @Test
+ void testFindAllArtists() {
+ db.insertArtist(new TrackedArtist("a", "Artist A", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("b", "Artist B", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ assertEquals(2, db.findAllArtists().size());
+ }
+
+ @Test
+ void testUpdateMonitoring() {
+ db.insertArtist(new TrackedArtist("m-1", "Test", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.updateMonitoring("m-1", new MonitoringFlags(false, true, false, false, false));
+
+ var artist = db.findByMbid("m-1").orElseThrow();
+ assertFalse(artist.albumMonitored());
+ assertTrue(artist.singleMonitored());
+ }
+
+ @Test
+ void testDeleteArtist() {
+ db.insertArtist(new TrackedArtist("del-1", "Delete Me", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ assertTrue(db.exists("del-1"));
+ db.deleteArtist("del-1");
+ assertFalse(db.exists("del-1"));
+ }
+
+ @Test
+ void testUpsertAndGetFeed() {
+ db.insertArtist(new TrackedArtist("feed-artist", "Feed Artist", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+
+ var groups = List.of(
+ new ReleaseGroup("rg-1", "Album One", "Album", "[]", "2020-01-01", "feed-artist", "Feed Artist", Instant.now()),
+ new ReleaseGroup("rg-2", "Single One", "Single", "[]", "2021-01-01", "feed-artist", "Feed Artist", Instant.now())
+ );
+ db.upsertReleaseGroups(groups);
+
+ var types = Map.of("feed-artist", List.of("Album", "Single"));
+ var result = db.getFeed(types, 10, 0);
+ assertEquals(2, result.entries().size());
+ assertFalse(result.hasMore());
+ }
+
+ @Test
+ void testGetFeedWithPagination() {
+ db.insertArtist(new TrackedArtist("page-artist", "Page Artist", "", "", true, true, false, false, false, Instant.now(), Instant.now()));
+
+ var groups = new ArrayList();
+ for (int i = 1; i <= 15; i++) {
+ groups.add(new ReleaseGroup("rg-" + i, "Release " + i, i % 2 == 0 ? "Album" : "Single", "[]",
+ "2020-" + String.format("%02d", i % 12 + 1) + "-01", "page-artist", "Page Artist", Instant.now()));
+ }
+ db.upsertReleaseGroups(groups);
+
+ var types = Map.of("page-artist", List.of("Album", "Single"));
+ var page1 = db.getFeed(types, 10, 0);
+ assertEquals(10, page1.entries().size());
+ assertTrue(page1.hasMore());
+
+ var page2 = db.getFeed(types, 10, 10);
+ assertEquals(5, page2.entries().size());
+ assertFalse(page2.hasMore());
+ }
+
+ @Test
+ void testSettings() {
+ assertEquals("8", db.getSetting("cache_ttl_hours", "8"));
+ db.setSetting("cache_ttl_hours", "12");
+ assertEquals("12", db.getSetting("cache_ttl_hours", "8"));
+ assertEquals("default", db.getSetting("nonexistent", "default"));
+ }
+
+ @Test
+ void testGetStaleness() {
+ db.insertArtist(new TrackedArtist("stale-1", "Stale Artist", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("stale-2", "Fresh Artist", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+
+ var groups = List.of(
+ new ReleaseGroup("rg-s1", "Release", "Album", "[]", "2020-01-01", "stale-2", "Fresh Artist", Instant.now())
+ );
+ db.upsertReleaseGroups(groups);
+
+ var staleness = db.getStaleness(List.of("stale-1", "stale-2"), 1000);
+ assertTrue(staleness.get("stale-1"));
+ assertFalse(staleness.get("stale-2"));
+ }
+
+ @Test
+ void testDeleteArtistCascading() {
+ db.insertArtist(new TrackedArtist("cascade-1", "Cascade", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-c1", "R1", "Album", "[]", "2020-01-01", "cascade-1", "Cascade", Instant.now())
+ ));
+ assertEquals(1, db.countReleaseGroupsForArtist("cascade-1"));
+
+ db.deleteArtistCascading("cascade-1");
+ assertFalse(db.exists("cascade-1"));
+ assertEquals(0, db.countReleaseGroupsForArtist("cascade-1"));
+ }
+
+ @Test
+ void testFeedPerArtistFiltering() {
+ db.insertArtist(new TrackedArtist("art-a", "Artist A", "", "", true, false, false, false, false, Instant.now(), Instant.now()));
+ db.insertArtist(new TrackedArtist("art-b", "Artist B", "", "", false, true, false, false, false, Instant.now(), Instant.now()));
+
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("rg-a1", "A Album", "Album", "[]", "2020-01-01", "art-a", "Artist A", Instant.now()),
+ new ReleaseGroup("rg-b1", "B Single", "Single", "[]", "2020-02-01", "art-b", "Artist B", Instant.now()),
+ new ReleaseGroup("rg-b2", "B Album", "Album", "[]", "2020-03-01", "art-b", "Artist B", Instant.now())
+ ));
+
+ var types = Map.of(
+ "art-a", List.of("Album"),
+ "art-b", List.of("Single")
+ );
+ var result = db.getFeed(types, 10, 0);
+ assertEquals(2, result.entries().size()); // A-Album + B-Single. B-Album excluded.
+ }
+}
diff --git a/src/test/java/com/discdrop/DtoTest.java b/src/test/java/com/discdrop/DtoTest.java
new file mode 100644
index 0000000..4978ac1
--- /dev/null
+++ b/src/test/java/com/discdrop/DtoTest.java
@@ -0,0 +1,87 @@
+package com.discdrop;
+
+import com.discdrop.client.dto.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DtoTest {
+
+ final ObjectMapper json = new ObjectMapper();
+
+ @Test
+ void testArtistSearchParse() throws Exception {
+ var input = "{\"artists\":[{\"id\":\"mbid-1\",\"name\":\"Test Artist\",\"disambiguation\":\"rock band\",\"area\":{\"name\":\"UK\"},\"type\":\"Group\",\"country\":\"GB\"}],\"count\":1}";
+ var result = json.readValue(input, ArtistSearchResponse.class);
+ assertEquals(1, result.count());
+ assertEquals("Test Artist", result.artists().get(0).name());
+ assertEquals("rock band", result.artists().get(0).disambiguation());
+ assertEquals("UK", result.artists().get(0).area().name());
+ assertEquals("Group", result.artists().get(0).type());
+ }
+
+ @Test
+ void testArtistSearchWithNoResults() throws Exception {
+ var input = "{\"artists\":[],\"count\":0}";
+ var result = json.readValue(input, ArtistSearchResponse.class);
+ assertTrue(result.artists().isEmpty());
+ assertEquals(0, result.count());
+ }
+
+ @Test
+ void testArtistDetailsParse() throws Exception {
+ var input = "{\"id\":\"mbid-d-1\",\"name\":\"Detail Artist\",\"disambiguation\":\"\",\"area\":{\"name\":\"US\"},\"type\":\"Person\",\"country\":\"US\",\"relations\":[]}";
+ var result = json.readValue(input, ArtistDetailsResponse.class);
+ assertEquals("Detail Artist", result.name());
+ assertEquals("US", result.area().name());
+ assertTrue(result.relations().isEmpty());
+ }
+
+ @Test
+ void testArtistDetailsWithRelations() throws Exception {
+ var input = "{\"id\":\"mbid-r-1\",\"name\":\"Related\",\"area\":null,\"type\":\"Group\",\"country\":\"DE\",\"relations\":[{\"type\":\"allmusic\",\"url\":{\"resource\":\"https://allmusic.com/artist/123\"}}]}";
+ var result = json.readValue(input, ArtistDetailsResponse.class);
+ assertEquals(1, result.relations().size());
+ assertEquals("allmusic", result.relations().get(0).type());
+ assertEquals("https://allmusic.com/artist/123", result.relations().get(0).url().resource());
+ }
+
+ @Test
+ void testReleaseGroupBrowseParse() throws Exception {
+ var input = "{\"release-groups\":[{\"id\":\"rg-1\",\"title\":\"Test Album\",\"first-release-date\":\"2020-01-15\",\"primary-type\":\"Album\",\"secondary-types\":[\"Live\"],\"artist-credit\":[{\"artist\":{\"id\":\"art-1\",\"name\":\"Test Artist\"}}]}],\"release-group-count\":1}";
+ var result = json.readValue(input, ReleaseGroupBrowseResponse.class);
+ assertEquals(1, result.releaseGroupCount());
+ var rg = result.releaseGroups().get(0);
+ assertEquals("Test Album", rg.title());
+ assertEquals("Album", rg.primaryType());
+ assertEquals("2020-01-15", rg.firstReleaseDate());
+ assertEquals(1, rg.secondaryTypes().size());
+ assertEquals("Live", rg.secondaryTypes().get(0));
+ assertEquals("Test Artist", rg.artistCredit().get(0).artist().name());
+ }
+
+ @Test
+ void testReleaseGroupBrowseEmpty() throws Exception {
+ var input = "{\"release-groups\":[],\"release-group-count\":0}";
+ var result = json.readValue(input, ReleaseGroupBrowseResponse.class);
+ assertTrue(result.releaseGroups().isEmpty());
+ assertEquals(0, result.releaseGroupCount());
+ }
+
+ @Test
+ void testUnknownPropertiesIgnored() throws Exception {
+ var input = "{\"id\":\"x\",\"name\":\"Test\",\"unknown_field\":true,\"area\":{\"name\":\"US\"},\"type\":\"Group\",\"country\":\"US\",\"relations\":[]}";
+ var result = json.readValue(input, ArtistDetailsResponse.class);
+ assertEquals("Test", result.name());
+ }
+
+ @Test
+ void testArtistSearchPartialData() throws Exception {
+ var input = "{\"artists\":[{\"id\":\"m-1\",\"name\":\"Minimal\"}],\"count\":1}";
+ var result = json.readValue(input, ArtistSearchResponse.class);
+ assertEquals("Minimal", result.artists().get(0).name());
+ assertNull(result.artists().get(0).area());
+ assertNull(result.artists().get(0).type());
+ }
+}
diff --git a/src/test/java/com/discdrop/FeedServiceTest.java b/src/test/java/com/discdrop/FeedServiceTest.java
new file mode 100644
index 0000000..b65c146
--- /dev/null
+++ b/src/test/java/com/discdrop/FeedServiceTest.java
@@ -0,0 +1,100 @@
+package com.discdrop;
+
+import com.discdrop.service.FeedService;
+import com.discdrop.service.CacheService;
+import com.discdrop.db.DatabaseService;
+import com.discdrop.db.Record.TrackedArtist;
+import com.discdrop.db.Record.ReleaseGroup;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class FeedServiceTest {
+
+ @Inject
+ FeedService feedService;
+
+ @Inject
+ CacheService cacheService;
+
+ @Inject
+ DatabaseService db;
+
+ @BeforeEach
+ void cleanUp() {
+ for (var a : db.findAllArtists()) {
+ db.deleteArtistCascading(a.mbid());
+ }
+ db.setSetting("cache_ttl_hours", "100");
+ }
+
+ @Test
+ void testEmptyFeed() {
+ assertTrue(feedService.getFeedEntries(1).entries().isEmpty());
+ assertTrue(feedService.getAllFeedEntries().isEmpty());
+ }
+
+ @Test
+ void testFeedWithData() {
+ db.insertArtist(new TrackedArtist("feed-a", "Feed A", "", "",
+ true, true, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("feed-rg-1", "Album One", "Album", "[]", "2020-01-01", "feed-a", "Feed A", Instant.now()),
+ new ReleaseGroup("feed-rg-2", "Single One", "Single", "[]", "2020-02-01", "feed-a", "Feed A", Instant.now())
+ ));
+
+ assertEquals(2, feedService.getAllFeedEntries().size());
+ }
+
+ @Test
+ void testPagination() {
+ db.insertArtist(new TrackedArtist("page-artist", "Page A", "", "",
+ true, true, true, false, false, Instant.now(), Instant.now()));
+ var groups = new java.util.ArrayList();
+ for (int i = 1; i <= 25; i++) {
+ groups.add(new ReleaseGroup("page-rg-" + i, "Rel " + i, i % 3 == 0 ? "EP" : (i % 2 == 0 ? "Single" : "Album"), "[]",
+ "2020-" + (i % 12 + 1) + "-01", "page-artist", "Page A", Instant.now()));
+ }
+ db.upsertReleaseGroups(groups);
+
+ var page1 = feedService.getFeedEntries(1);
+ assertEquals(10, page1.entries().size());
+ assertTrue(page1.hasMore());
+
+ var page3 = feedService.getFeedEntries(3);
+ assertEquals(5, page3.entries().size());
+ assertFalse(page3.hasMore());
+ }
+
+ @Test
+ void testMonitoringFiltersFeed() {
+ db.insertArtist(new TrackedArtist("mon-artist", "Monitored", "", "",
+ true, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("mon-rg-1", "Album", "Album", "[]", "2020-01-01", "mon-artist", "Monitored", Instant.now()),
+ new ReleaseGroup("mon-rg-2", "Single", "Single", "[]", "2020-02-01", "mon-artist", "Monitored", Instant.now())
+ ));
+
+ var feed = feedService.getAllFeedEntries();
+ assertEquals(1, feed.size());
+ assertEquals("Album", feed.get(0).primaryType());
+ }
+
+ @Test
+ void testNoMonitoredTypesShowsNothing() {
+ db.insertArtist(new TrackedArtist("none-artist", "None", "", "",
+ false, false, false, false, false, Instant.now(), Instant.now()));
+ db.upsertReleaseGroups(List.of(
+ new ReleaseGroup("none-rg-1", "Album", "Album", "[]", "2020-01-01", "none-artist", "None", Instant.now())
+ ));
+
+ assertTrue(feedService.getAllFeedEntries().isEmpty());
+ }
+}
diff --git a/src/test/java/com/discdrop/RSSFeedBuilderTest.java b/src/test/java/com/discdrop/RSSFeedBuilderTest.java
new file mode 100644
index 0000000..30bb3c3
--- /dev/null
+++ b/src/test/java/com/discdrop/RSSFeedBuilderTest.java
@@ -0,0 +1,89 @@
+package com.discdrop;
+
+import com.discdrop.db.Record.ReleaseGroup;
+import com.discdrop.util.RSSFeedBuilder;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@QuarkusTest
+class RSSFeedBuilderTest {
+
+ @Inject
+ RSSFeedBuilder feedBuilder;
+
+ @Test
+ void testEmptyRss() {
+ var xml = feedBuilder.buildRss(List.of());
+ assertTrue(xml.contains("DiscDrop"));
+ assertTrue(xml.contains(""));
+ }
+
+ @Test
+ void testRssWithEntries() {
+ var entries = List.of(
+ new ReleaseGroup("mbid-1", "Album One", "Album", "[]", "2020-01-15", "artist-1", "Artist One", Instant.now()),
+ new ReleaseGroup("mbid-2", "Single Two", "Single", "[\"Live\"]", "2021-06-01", "artist-2", "Artist Two", Instant.now())
+ );
+ var xml = feedBuilder.buildRss(entries);
+
+ assertTrue(xml.contains("Artist One"));
+ assertTrue(xml.contains("Album One"));
+ assertTrue(xml.contains("mbid-1"));
+ assertTrue(xml.contains("Artist Two"));
+ assertTrue(xml.contains("Single Two"));
+ assertTrue(xml.contains("mbid-2"));
+ assertTrue(xml.contains("Album"));
+ assertTrue(xml.contains("Single"));
+ assertTrue(xml.contains("2020-01-15"));
+ assertTrue(xml.contains("2021-06-01"));
+ assertTrue(xml.contains("coverartarchive"));
+ }
+
+ @Test
+ void testOpml() {
+ var opml = feedBuilder.buildOpml();
+ assertTrue(opml.contains("", "Album", "[]", "2020-01-01", "artist-5", "Artist & Band", Instant.now())
+ );
+ var xml = feedBuilder.buildRss(entries);
+ assertTrue(xml.contains("&") || xml.contains("&"));
+ assertTrue(xml.contains("<") || xml.contains("<"));
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000..96c7235
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,7 @@
+quarkus.http.port=0
+
+discdrop.db.path=target/test-discdrop.db
+discdrop.cache.ttl-hours=100
+discdrop.base-url=http://localhost:8080
+
+discdrop.user-agent=DiscDrop/1.0 (test)