diff --git a/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetIssueWithDetailsAction.java b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetIssueWithDetailsAction.java index 2f2d963..ccd61ce 100644 --- a/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetIssueWithDetailsAction.java +++ b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetIssueWithDetailsAction.java @@ -75,7 +75,7 @@ public ComicVineIssue execute() throws MetadataException { try { result = request.block(); } catch (Exception error) { - throw new MetadataException("failed to get issue details", error); + throw new MetadataException("failed to get story details", error); } if (result == null) throw new MetadataException("No response received"); diff --git a/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesAction.java b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesAction.java new file mode 100644 index 0000000..af49ff4 --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesAction.java @@ -0,0 +1,147 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.metadata.comicvine.actions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.comixedproject.metadata.MetadataException; +import org.comixedproject.metadata.comicvine.model.ComicVineGetStoryListResponse; +import org.comixedproject.metadata.model.StoryMetadata; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +/** + * ComicVineGetStoriesAction defines an action that loads a list of candidates when + * scraping a story arc. + * + * @author Darryl L. Pierce + */ +@Log4j2 +public class ComicVineGetStoriesAction + extends AbstractComicVineScrapingAction> { + @Getter @Setter private String storyName; + @Getter @Setter private Integer maxRecords; + private int page; + + @Override + public List execute() throws MetadataException { + this.doCheckSetup(); + + this.addFilter(NAME_FILTER, this.storyName); + + this.addField("id"); + this.addField("name"); + this.addField("publisher"); + this.addField("image"); + + this.addParameter(RESOURCES_PARAMETER, "volume"); + this.addParameter(QUERY_PARAMETER, this.storyName); + if (maxRecords > 0) this.addParameter(RESULT_LIMIT_PARAMETER, String.valueOf(this.maxRecords)); + + List result = new ArrayList<>(); + boolean done = false; + + while (!done) { + this.doIncrementPage(); + + log.debug( + "Creating url for: API key=****{} story name={}", this.getMaskedApiKey(), this.storyName); + final String url = this.createUrl(this.baseUrl, "story_arcs"); + final WebClient client = this.createWebClient(url); + final Mono request = + client.get().uri(url).retrieve().bodyToMono(ComicVineGetStoryListResponse.class); + ComicVineGetStoryListResponse response = null; + + try { + response = request.block(); + } catch (Exception error) { + throw new MetadataException("Failed to get response", error); + } + + if (response == null) { + throw new MetadataException("Failed to receive a response"); + } + log.debug("Received: {} stories", response.getResults().size()); + + Integer totalRecords = maxRecords; + if (totalRecords == 0 || totalRecords >= response.getResults().size()) + totalRecords = response.getResults().size(); + + response + .getResults() + .subList(0, totalRecords) + .forEach( + storyListEntry -> { + log.trace( + "Processing story record: {} name={}", + storyListEntry.getReferenceId(), + storyListEntry.getName()); + final StoryMetadata entry = new StoryMetadata(); + entry.setReferenceId(storyListEntry.getReferenceId()); + if (Objects.nonNull(storyListEntry.getPublisher())) { + entry.setPublisher(storyListEntry.getPublisher().getName()); + } + entry.setName(storyListEntry.getName()); + entry.setImageUrl(storyListEntry.getImages().get("original_url")); + result.add(entry); + }); + + done = + (hitMaxRecordLimit(result)) + || (response.getOffset() + response.getNumberOfPageResults()) + >= response.getNumberOfTotalResults(); + + if (!done) { + log.trace("Sleeping for {}s", this.getDelay()); + try { + Thread.sleep(this.getDelay() * 1000L); + } catch (InterruptedException error) { + log.error("ComicVine get volumes action interrupted", error); + throw error; + } + } + } + + return result; + } + + private void doIncrementPage() { + log.trace("Incremented page value: {}", this.page); + this.page++; + if (this.page > 1) { + log.trace("Setting page: {}", this.page); + this.addParameter(PAGE_PARAMETER, String.valueOf(this.page)); + } + } + + private void doCheckSetup() throws MetadataException { + if (!StringUtils.hasLength(this.getApiKey())) throw new MetadataException("Missing API key"); + if (!StringUtils.hasLength(this.storyName)) throw new MetadataException("Missing story name"); + if (maxRecords == null) throw new MetadataException("Missing maximum records"); + } + + private boolean hitMaxRecordLimit(final List records) { + return this.maxRecords > 0 && records.size() == this.maxRecords; + } +} diff --git a/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailAction.java b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailAction.java new file mode 100644 index 0000000..f840602 --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailAction.java @@ -0,0 +1,110 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.metadata.comicvine.actions; + +import java.util.Objects; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; +import org.comixedproject.metadata.MetadataException; +import org.comixedproject.metadata.comicvine.model.ComicVineGetStoryDetailResponse; +import org.comixedproject.metadata.comicvine.model.ComicVineIssue; +import org.comixedproject.metadata.model.IssueDetailsMetadata; +import org.comixedproject.metadata.model.StoryDetailMetadata; +import org.comixedproject.metadata.model.StoryIssueMetadata; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +/** + * ComicVineGetStoryDetailAction fetches the metadata for a single story arc from + * ComicVine. + * + * @author Darryl L. Pierce + */ +@NoArgsConstructor +@Log4j2 +public class ComicVineGetStoryDetailAction + extends AbstractComicVineScrapingAction { + @Getter @Setter private String referenceId; + + ComicVineGetIssueDetailsAction getIssueDetailsAction = new ComicVineGetIssueDetailsAction(); + + @Override + public StoryDetailMetadata execute() throws MetadataException { + if (!StringUtils.hasLength(this.getApiKey())) throw new MetadataException("Missing API key"); + if (this.referenceId == null) throw new MetadataException("Missing reference id"); + + this.addField("id"); + this.addField("publisher"); + this.addField("name"); + this.addField("description"); + this.addField("issues"); + + final StoryDetailMetadata result = new StoryDetailMetadata(); + log.debug( + "Creating url for: API key=****{} reference id={}", + this.getMaskedApiKey(), + this.referenceId); + final String url = + this.createUrl(this.baseUrl, String.format("story_arc/4045-%s", this.referenceId)); + final WebClient client = this.createWebClient(url); + + final Mono request = + client.get().uri(url).retrieve().bodyToMono(ComicVineGetStoryDetailResponse.class); + + ComicVineGetStoryDetailResponse response = null; + + try { + response = request.block(); + } catch (Exception error) { + throw new MetadataException("Failed to get response", error); + } + + if (response == null) throw new MetadataException("No response received"); + + log.debug("Received response with {} issue(s)", response.getResults().getIssues().size()); + + result.setReferenceId(this.referenceId); + if (Objects.nonNull(response.getResults().getPublisher())) { + result.setPublisher(response.getResults().getPublisher().getName().trim()); + } + result.setName(response.getResults().getName()); + result.setDescription(response.getResults().getDescription()); + this.getIssueDetailsAction.setBaseUrl(this.baseUrl); + this.getIssueDetailsAction.setApiKey(this.getApiKey()); + + for (int index = 0; index < response.getResults().getIssues().size(); index++) { + final ComicVineIssue entry = response.getResults().getIssues().get(index); + this.getIssueDetailsAction.setIssueId(entry.getId()); + + final IssueDetailsMetadata issue = this.getIssueDetailsAction.execute(); + final StoryIssueMetadata issueMetadata = new StoryIssueMetadata(); + issueMetadata.setReadingOrder(index + 1); + issueMetadata.setName(issue.getSeries()); + issueMetadata.setVolume(issue.getVolume()); + issueMetadata.setIssueNumber(issue.getIssueNumber()); + issueMetadata.setCoverDate(issue.getCoverDate()); + result.getIssues().add(issueMetadata); + } + + return result; + } +} diff --git a/src/main/java/org/comixedproject/metadata/comicvine/adaptors/ComicVineMetadataAdaptor.java b/src/main/java/org/comixedproject/metadata/comicvine/adaptors/ComicVineMetadataAdaptor.java index 89f35be..b6d2013 100644 --- a/src/main/java/org/comixedproject/metadata/comicvine/adaptors/ComicVineMetadataAdaptor.java +++ b/src/main/java/org/comixedproject/metadata/comicvine/adaptors/ComicVineMetadataAdaptor.java @@ -28,13 +28,8 @@ import org.comixedproject.metadata.MetadataException; import org.comixedproject.metadata.adaptors.AbstractMetadataAdaptor; import org.comixedproject.metadata.adaptors.MetadataAdaptor; -import org.comixedproject.metadata.comicvine.actions.ComicVineGetAllIssuesAction; -import org.comixedproject.metadata.comicvine.actions.ComicVineGetIssueAction; -import org.comixedproject.metadata.comicvine.actions.ComicVineGetIssueDetailsAction; -import org.comixedproject.metadata.comicvine.actions.ComicVineGetVolumesAction; -import org.comixedproject.metadata.model.IssueDetailsMetadata; -import org.comixedproject.metadata.model.IssueMetadata; -import org.comixedproject.metadata.model.VolumeMetadata; +import org.comixedproject.metadata.comicvine.actions.*; +import org.comixedproject.metadata.model.*; import org.comixedproject.model.metadata.MetadataSource; /** @@ -57,6 +52,44 @@ public ComicVineMetadataAdaptor() { super("ComiXed ComicVine Scraper", PROVIDER_NAME); } + @Override + public List getStories( + final String storyName, final Integer maxRecords, final MetadataSource metadataSource) + throws MetadataException { + log.debug("Fetching stories from ComicVine: storyName={}", storyName); + final ComicVineGetStoriesAction action = new ComicVineGetStoriesAction(); + action.setBaseUrl(BASE_URL); + action.setApiKey( + this.getSourcePropertyByName(metadataSource.getProperties(), PROPERTY_API_KEY, true)); + action.setDelay(this.doGetDelayValue(metadataSource)); + action.setStoryName(storyName); + action.setMaxRecords(maxRecords); + + log.debug("Executing action"); + final List result = action.execute(); + + log.debug("Returning {} stories", result.size()); + return result; + } + + @Override + public StoryDetailMetadata getStory(final String referenceId, final MetadataSource metadataSource) + throws MetadataException { + log.debug("Fetching story details: referenceId={}", referenceId); + final ComicVineGetStoryDetailAction action = new ComicVineGetStoryDetailAction(); + action.setBaseUrl(BASE_URL); + action.setApiKey( + this.getSourcePropertyByName(metadataSource.getProperties(), PROPERTY_API_KEY, true)); + action.setDelay(this.doGetDelayValue(metadataSource)); + action.setReferenceId(referenceId); + + log.debug("Executing action"); + final StoryDetailMetadata result = action.execute(); + + log.debug("Returning one story with {} issues", result.getIssues().size()); + return result; + } + @Override public List getVolumes( final String seriesName, final Integer maxRecords, final MetadataSource metadataSource) diff --git a/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryDetailResponse.java b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryDetailResponse.java new file mode 100644 index 0000000..616f1e3 --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryDetailResponse.java @@ -0,0 +1,12 @@ +package org.comixedproject.metadata.comicvine.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class ComicVineGetStoryDetailResponse extends AbstractComicVineQueryResponse { + @JsonProperty("results") + @Getter + private ComicVineStoryDetailResponse results; +} diff --git a/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryListResponse.java b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryListResponse.java new file mode 100644 index 0000000..009996a --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineGetStoryListResponse.java @@ -0,0 +1,35 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.metadata.comicvine.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; + +/** + * ComicVineGetStoryListResponse represents the paylaod returned when scraping a list + * of stories. + * + * @author Darryl L. Pierce + */ +public class ComicVineGetStoryListResponse extends AbstractComicVineQueryResponse { + @JsonProperty("results") + @Getter + private List results; +} diff --git a/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryDetailResponse.java b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryDetailResponse.java new file mode 100644 index 0000000..db925eb --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryDetailResponse.java @@ -0,0 +1,27 @@ +package org.comixedproject.metadata.comicvine.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; + +public class ComicVineStoryDetailResponse { + @JsonProperty("id") + @Getter + private String id; + + @JsonProperty("publisher") + @Getter + private ComicVinePublisher publisher; + + @JsonProperty("name") + @Getter + private String name; + + @JsonProperty("description") + @Getter + private String description; + + @JsonProperty("issues") + @Getter + private List issues; +} diff --git a/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryListEntry.java b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryListEntry.java new file mode 100644 index 0000000..59f6822 --- /dev/null +++ b/src/main/java/org/comixedproject/metadata/comicvine/model/ComicVineStoryListEntry.java @@ -0,0 +1,46 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.metadata.comicvine.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +public class ComicVineStoryListEntry { + @JsonProperty("name") + @Getter + @Setter + private String name; + + @JsonProperty("publisher") + @Getter + @Setter + private ComicVinePublisher publisher; + + @JsonProperty("id") + @Getter + @Setter + private String referenceId; + + @JsonProperty("image") + @Getter + @Setter + private Map images; +} diff --git a/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesActionTest.java b/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesActionTest.java new file mode 100644 index 0000000..c499e44 --- /dev/null +++ b/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoriesActionTest.java @@ -0,0 +1,128 @@ +package org.comixedproject.metadata.comicvine.actions; + +import static junit.framework.TestCase.*; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.apache.commons.io.FileUtils; +import org.comixedproject.metadata.MetadataException; +import org.comixedproject.metadata.model.StoryMetadata; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +@RunWith(MockitoJUnitRunner.class) +public class ComicVineGetStoriesActionTest { + private static final String TEST_API_KEY = "OICU812"; + private static final long TEST_DELAY = 1L; + private static final String TEST_STORY_NAME = "\"The Mighty Avengers\" The Ultron Initiative"; + private static final String TEST_REFERENCE_ID = "54894"; + private static final String TEST_BAD_RESPONSE_BODY = "This is not JSON"; + private static final String TEST_RESPONSE_BODY_FILE = + "src/test/resources/story-list-metadata.json"; + private static final String TEST_IMAGE_URL = + "https://comicvine.gamespot.com/a/uploads/original/1/10390/390031-69520-the-ultron-initiativ.JPG"; + private static final String TEST_PUBLISHER_NAME = "Marvel"; + private static final Integer TEST_ALL_RECORDS = 7; + private static final Integer TEST_MAX_RECORDS = 3; + public MockWebServer comicVineServer; + + @InjectMocks private ComicVineGetStoriesAction action; + + private String responseBody; + + @Before + public void setUp() throws IOException { + responseBody = FileUtils.readFileToString(new File(TEST_RESPONSE_BODY_FILE), "UTF-8"); + comicVineServer = new MockWebServer(); + comicVineServer.start(); + + final String hostname = String.format("http://localhost:%s", this.comicVineServer.getPort()); + action.setBaseUrl(hostname); + action.setApiKey(TEST_API_KEY); + action.setDelay(TEST_DELAY); + action.setStoryName(TEST_STORY_NAME); + } + + @After + public void tearDown() throws IOException { + comicVineServer.shutdown(); + } + + @Test(expected = MetadataException.class) + public void testExecuteMissingApiKey() throws MetadataException { + action.setApiKey(""); + action.execute(); + } + + @Test(expected = MetadataException.class) + public void testExecuteMissingStoryName() throws MetadataException { + action.setStoryName(""); + + action.execute(); + } + + @Test(expected = MetadataException.class) + public void testExecuteBadResponse() throws MetadataException { + this.comicVineServer.enqueue( + new MockResponse() + .setBody(TEST_BAD_RESPONSE_BODY) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + action.execute(); + } + + @Test + public void testExecute() throws MetadataException { + this.comicVineServer.enqueue( + new MockResponse() + .setBody(this.responseBody) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + action.setMaxRecords(0); + + final List result = action.execute(); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(TEST_ALL_RECORDS.intValue(), result.size()); + + final StoryMetadata story = result.get(0); + + assertEquals(TEST_STORY_NAME, story.getName()); + assertEquals(TEST_PUBLISHER_NAME, story.getPublisher()); + assertEquals(TEST_REFERENCE_ID, story.getReferenceId()); + assertEquals(TEST_IMAGE_URL, story.getImageUrl()); + } + + @Test + public void testExecuteWithLimit() throws MetadataException { + this.comicVineServer.enqueue( + new MockResponse() + .setBody(this.responseBody) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + action.setMaxRecords(TEST_MAX_RECORDS); + + final List result = action.execute(); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(TEST_MAX_RECORDS.intValue(), result.size()); + + final StoryMetadata story = result.get(0); + + assertEquals(TEST_STORY_NAME, story.getName()); + assertEquals(TEST_PUBLISHER_NAME, story.getPublisher()); + assertEquals(TEST_REFERENCE_ID, story.getReferenceId()); + assertEquals(TEST_IMAGE_URL, story.getImageUrl()); + } +} diff --git a/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailActionTest.java b/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailActionTest.java new file mode 100644 index 0000000..0270b70 --- /dev/null +++ b/src/test/java/org/comixedproject/metadata/comicvine/actions/ComicVineGetStoryDetailActionTest.java @@ -0,0 +1,89 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2025, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.metadata.comicvine.actions; + +import static junit.framework.TestCase.assertNotNull; + +import java.io.IOException; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.comixedproject.metadata.MetadataException; +import org.comixedproject.metadata.model.IssueDetailsMetadata; +import org.comixedproject.metadata.model.StoryDetailMetadata; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +@RunWith(MockitoJUnitRunner.class) +public class ComicVineGetStoryDetailActionTest { + private static final String TEST_API_KEY = "This.Is.A.Test.Key"; + private static final String TEST_STORY_RESPONSE = + "{\"error\":\"OK\",\"limit\":1,\"offset\":0,\"number_of_page_results\":1,\"number_of_total_results\":1,\"status_code\":1,\"results\":{\"description\":null,\"id\":61035,\"issues\":[{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-935767\\/\",\"id\":935767,\"name\":\"The Red Fist Saga Part 1; The Island\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-1-the-red-fist-saga-part-1-the-island\\/4000-935767\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-942720\\/\",\"id\":942720,\"name\":\"The Red Fist Saga Part 2; The Hand\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-2-the-red-fist-saga-part-2-the-hand\\/4000-942720\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-946713\\/\",\"id\":946713,\"name\":\"The Red Fist Saga Part 3\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-3-the-red-fist-saga-part-3\\/4000-946713\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-950374\\/\",\"id\":950374,\"name\":\"The Red Fist Saga Part 4\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-4-the-red-fist-saga-part-4\\/4000-950374\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-956788\\/\",\"id\":956788,\"name\":\"The Red Fist Saga Part 5\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-5-the-red-fist-saga-part-5\\/4000-956788\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-980931\\/\",\"id\":980931,\"name\":\"Vol. 1: The Red Fist Saga Part One\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-and-elektra-by-chip-zdarsky-1-vol-1-the-\\/4000-980931\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-958981\\/\",\"id\":958981,\"name\":\"The Red Fist Saga Part 6\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-6-the-red-fist-saga-part-6\\/4000-958981\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-963989\\/\",\"id\":963989,\"name\":\"The Red Fist Saga Part 7\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-7-the-red-fist-saga-part-7\\/4000-963989\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-969380\\/\",\"id\":969380,\"name\":\"The Red Fist Saga Part 8\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-8-the-red-fist-saga-part-8\\/4000-969380\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-979431\\/\",\"id\":979431,\"name\":\"The Red Fist Saga Part 9\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-9-the-red-fist-saga-part-9\\/4000-979431\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-985648\\/\",\"id\":985648,\"name\":\"The Red Fist Saga Part 10\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-10-the-red-fist-saga-part-10\\/4000-985648\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-988121\\/\",\"id\":988121,\"name\":\"The Red Fist Saga Part 11; Painful Lesson\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-11-the-red-fist-saga-part-11-painful-les\\/4000-988121\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-1003838\\/\",\"id\":1003838,\"name\":\"Vol. 2: The Red Fist Saga Part Two\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-and-elektra-by-chip-zdarsky-2-vol-2-the-\\/4000-1003838\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-993228\\/\",\"id\":993228,\"name\":\"The Red Fist Saga Part 12\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-12-the-red-fist-saga-part-12\\/4000-993228\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-998583\\/\",\"id\":998583,\"name\":\"The Red Fist Saga, Part 13\",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-13-the-red-fist-saga-part-13\\/4000-998583\\/\"},{\"api_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/api\\/issue\\/4000-1008950\\/\",\"id\":1008950,\"name\":\"The Red Fist Saga, Conclusion \",\"site_detail_url\":\"https:\\/\\/comicvine.gamespot.com\\/daredevil-14-the-red-fist-saga-conclusion\\/4000-1008950\\/\"}],\"name\":\"\\\"Daredevil\\\" The Red Fist Saga\",\"publisher\":null},\"version\":\"1.0\"}"; + private static final String TEST_REFERENCE_ID = "337"; + + @InjectMocks private ComicVineGetStoryDetailAction action; + @Mock private ComicVineGetIssueDetailsAction getIssueDetailsAction; + @Mock private IssueDetailsMetadata storyIssueMetadata; + + private MockWebServer comicVineServer; + + @Before + public void setUp() throws IOException { + action.getIssueDetailsAction = getIssueDetailsAction; + + comicVineServer = new MockWebServer(); + comicVineServer.start(); + + final String hostname = String.format("http://localhost:%s", this.comicVineServer.getPort()); + action.setBaseUrl(hostname); + action.setApiKey(TEST_API_KEY); + action.setReferenceId(TEST_REFERENCE_ID); + } + + @Test(expected = MetadataException.class) + public void testExecuteFailsWithoutApiKey() throws MetadataException { + action.setApiKey(""); + action.execute(); + } + + @Test(expected = MetadataException.class) + public void testExecuteFailsWithoutReferenceId() throws MetadataException { + action.setReferenceId(null); + action.execute(); + } + + @Test + public void testExecute() throws MetadataException { + this.comicVineServer.enqueue( + new MockResponse() + .setBody(TEST_STORY_RESPONSE) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + Mockito.when(getIssueDetailsAction.execute()).thenReturn(storyIssueMetadata); + + final StoryDetailMetadata result = action.execute(); + + assertNotNull(result); + } +} diff --git a/src/test/resources/story-list-metadata.json b/src/test/resources/story-list-metadata.json new file mode 100644 index 0000000..8aa8843 --- /dev/null +++ b/src/test/resources/story-list-metadata.json @@ -0,0 +1,270 @@ +{ + "error": "OK", + "limit": 100, + "offset": 0, + "number_of_page_results": 7, + "number_of_total_results": 7, + "status_code": 1, + "results": [ + { + "aliases": null, + "api_detail_url": "https:\/\/comicvine.gamespot.com\/api\/story_arc\/4045-54894\/", + "count_of_isssue_appearances": 0, + "date_added": "2008-06-06 11:27:52", + "date_last_updated": "2024-03-03 10:17:38", + "deck": "\"The Ultron Initiative\" is a comic book arc running through #1-6 of (The) Mighty Avengers. This is the first arc of this title, and is a tie-in of the Initiative.", + "description": "

Overview<\/h2>