Backend implementation

This commit is contained in:
Stefan Hardegger
2025-07-21 10:46:11 +02:00
parent 68c7c8115f
commit bebb799784
29 changed files with 10303 additions and 5 deletions

View File

@@ -0,0 +1,190 @@
package com.storycove.entity;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Author Entity Tests")
class AuthorTest {
private Validator validator;
private Author author;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
author = new Author("Test Author");
}
@Test
@DisplayName("Should create author with valid name")
void shouldCreateAuthorWithValidName() {
assertEquals("Test Author", author.getName());
assertNotNull(author.getStories());
assertNotNull(author.getUrls());
assertEquals(0.0, author.getAverageStoryRating());
assertEquals(0, author.getTotalStoryRatings());
}
@Test
@DisplayName("Should fail validation when name is blank")
void shouldFailValidationWhenNameIsBlank() {
author.setName("");
Set<ConstraintViolation<Author>> violations = validator.validate(author);
assertEquals(1, violations.size());
assertEquals("Author name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name is null")
void shouldFailValidationWhenNameIsNull() {
author.setName(null);
Set<ConstraintViolation<Author>> violations = validator.validate(author);
assertEquals(1, violations.size());
assertEquals("Author name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name exceeds 255 characters")
void shouldFailValidationWhenNameTooLong() {
String longName = "a".repeat(256);
author.setName(longName);
Set<ConstraintViolation<Author>> violations = validator.validate(author);
assertEquals(1, violations.size());
assertEquals("Author name must not exceed 255 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when bio exceeds 1000 characters")
void shouldFailValidationWhenBioTooLong() {
String longBio = "a".repeat(1001);
author.setBio(longBio);
Set<ConstraintViolation<Author>> violations = validator.validate(author);
assertEquals(1, violations.size());
assertEquals("Bio must not exceed 1000 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should add and remove stories correctly")
void shouldAddAndRemoveStoriesCorrectly() {
Story story1 = new Story("Story 1");
Story story2 = new Story("Story 2");
author.addStory(story1);
author.addStory(story2);
assertEquals(2, author.getStories().size());
assertTrue(author.getStories().contains(story1));
assertTrue(author.getStories().contains(story2));
assertEquals(author, story1.getAuthor());
assertEquals(author, story2.getAuthor());
author.removeStory(story1);
assertEquals(1, author.getStories().size());
assertFalse(author.getStories().contains(story1));
assertNull(story1.getAuthor());
}
@Test
@DisplayName("Should add and remove URLs correctly")
void shouldAddAndRemoveUrlsCorrectly() {
String url1 = "https://example.com/author1";
String url2 = "https://example.com/author2";
author.addUrl(url1);
author.addUrl(url2);
assertEquals(2, author.getUrls().size());
assertTrue(author.getUrls().contains(url1));
assertTrue(author.getUrls().contains(url2));
author.addUrl(url1); // Should not add duplicate
assertEquals(2, author.getUrls().size());
author.removeUrl(url1);
assertEquals(1, author.getUrls().size());
assertFalse(author.getUrls().contains(url1));
}
@Test
@DisplayName("Should not add null or duplicate URLs")
void shouldNotAddNullOrDuplicateUrls() {
String url = "https://example.com/author";
author.addUrl(null);
assertEquals(0, author.getUrls().size());
author.addUrl(url);
author.addUrl(url);
assertEquals(1, author.getUrls().size());
}
@Test
@DisplayName("Should calculate average story rating correctly")
void shouldCalculateAverageStoryRatingCorrectly() {
// Initially no stories, should return 0.0
assertEquals(0.0, author.getAverageStoryRating());
assertEquals(0, author.getTotalStoryRatings());
// Add stories with ratings
Story story1 = new Story("Story 1");
story1.setAverageRating(4.0);
story1.setTotalRatings(5);
author.addStory(story1);
Story story2 = new Story("Story 2");
story2.setAverageRating(5.0);
story2.setTotalRatings(3);
author.addStory(story2);
Story story3 = new Story("Story 3");
story3.setAverageRating(3.0);
story3.setTotalRatings(2);
author.addStory(story3);
// Average should be (4.0 + 5.0 + 3.0) / 3 = 4.0
assertEquals(4.0, author.getAverageStoryRating());
assertEquals(10, author.getTotalStoryRatings()); // 5 + 3 + 2
// Add unrated story - should not affect average
Story unratedStory = new Story("Unrated Story");
unratedStory.setTotalRatings(0);
author.addStory(unratedStory);
assertEquals(4.0, author.getAverageStoryRating()); // Should remain the same
assertEquals(10, author.getTotalStoryRatings()); // Should remain the same
}
@Test
@DisplayName("Should handle equals and hashCode correctly")
void shouldHandleEqualsAndHashCodeCorrectly() {
Author author1 = new Author("Author 1");
Author author2 = new Author("Author 2");
Author author3 = new Author("Author 1");
assertNotEquals(author1, author2);
assertNotEquals(author1, author3); // Different because IDs are null
assertEquals(author1, author1);
assertNotEquals(author1, null);
assertNotEquals(author1, "not an author");
assertEquals(author1.hashCode(), author1.hashCode());
}
@Test
@DisplayName("Should have proper toString representation")
void shouldHaveProperToStringRepresentation() {
String toString = author.toString();
assertTrue(toString.contains("Test Author"));
assertTrue(toString.contains("Author{"));
}
}

View File

@@ -0,0 +1,210 @@
package com.storycove.entity;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Series Entity Tests")
class SeriesTest {
private Validator validator;
private Series series;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
series = new Series("The Chronicles of Narnia");
}
@Test
@DisplayName("Should create series with valid name")
void shouldCreateSeriesWithValidName() {
assertEquals("The Chronicles of Narnia", series.getName());
assertEquals(0, series.getTotalParts());
assertFalse(series.getIsComplete());
assertNotNull(series.getStories());
assertTrue(series.getStories().isEmpty());
}
@Test
@DisplayName("Should create series with name and description")
void shouldCreateSeriesWithNameAndDescription() {
Series seriesWithDesc = new Series("Harry Potter", "A series about a young wizard");
assertEquals("Harry Potter", seriesWithDesc.getName());
assertEquals("A series about a young wizard", seriesWithDesc.getDescription());
}
@Test
@DisplayName("Should fail validation when name is blank")
void shouldFailValidationWhenNameIsBlank() {
series.setName("");
Set<ConstraintViolation<Series>> violations = validator.validate(series);
assertEquals(1, violations.size());
assertEquals("Series name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name is null")
void shouldFailValidationWhenNameIsNull() {
series.setName(null);
Set<ConstraintViolation<Series>> violations = validator.validate(series);
assertEquals(1, violations.size());
assertEquals("Series name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name exceeds 255 characters")
void shouldFailValidationWhenNameTooLong() {
String longName = "a".repeat(256);
series.setName(longName);
Set<ConstraintViolation<Series>> violations = validator.validate(series);
assertEquals(1, violations.size());
assertEquals("Series name must not exceed 255 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when description exceeds 1000 characters")
void shouldFailValidationWhenDescriptionTooLong() {
String longDescription = "a".repeat(1001);
series.setDescription(longDescription);
Set<ConstraintViolation<Series>> violations = validator.validate(series);
assertEquals(1, violations.size());
assertEquals("Series description must not exceed 1000 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should add and remove stories correctly")
void shouldAddAndRemoveStoriesCorrectly() {
Story story1 = new Story("Part 1");
Story story2 = new Story("Part 2");
series.addStory(story1);
series.addStory(story2);
assertEquals(2, series.getStories().size());
assertEquals(2, series.getTotalParts());
assertTrue(series.getStories().contains(story1));
assertTrue(series.getStories().contains(story2));
assertEquals(series, story1.getSeries());
assertEquals(series, story2.getSeries());
series.removeStory(story1);
assertEquals(1, series.getStories().size());
assertEquals(1, series.getTotalParts());
assertFalse(series.getStories().contains(story1));
assertNull(story1.getSeries());
}
@Test
@DisplayName("Should get next story correctly")
void shouldGetNextStoryCorrectly() {
Story story1 = new Story("Part 1");
story1.setPartNumber(1);
Story story2 = new Story("Part 2");
story2.setPartNumber(2);
Story story3 = new Story("Part 3");
story3.setPartNumber(3);
series.addStory(story1);
series.addStory(story2);
series.addStory(story3);
assertEquals(story2, series.getNextStory(story1));
assertEquals(story3, series.getNextStory(story2));
assertNull(series.getNextStory(story3));
}
@Test
@DisplayName("Should get previous story correctly")
void shouldGetPreviousStoryCorrectly() {
Story story1 = new Story("Part 1");
story1.setPartNumber(1);
Story story2 = new Story("Part 2");
story2.setPartNumber(2);
Story story3 = new Story("Part 3");
story3.setPartNumber(3);
series.addStory(story1);
series.addStory(story2);
series.addStory(story3);
assertNull(series.getPreviousStory(story1));
assertEquals(story1, series.getPreviousStory(story2));
assertEquals(story2, series.getPreviousStory(story3));
}
@Test
@DisplayName("Should return null for next/previous when part number is null")
void shouldReturnNullForNextPreviousWhenPartNumberIsNull() {
Story storyWithoutPart = new Story("Story without part");
series.addStory(storyWithoutPart);
assertNull(series.getNextStory(storyWithoutPart));
assertNull(series.getPreviousStory(storyWithoutPart));
}
@Test
@DisplayName("Should handle equals and hashCode correctly")
void shouldHandleEqualsAndHashCodeCorrectly() {
Series series1 = new Series("Series 1");
Series series2 = new Series("Series 2");
Series series3 = new Series("Series 1");
assertNotEquals(series1, series2);
assertNotEquals(series1, series3); // Different because IDs are null
assertEquals(series1, series1);
assertNotEquals(series1, null);
assertNotEquals(series1, "not a series");
assertEquals(series1.hashCode(), series1.hashCode());
}
@Test
@DisplayName("Should have proper toString representation")
void shouldHaveProperToStringRepresentation() {
String toString = series.toString();
assertTrue(toString.contains("The Chronicles of Narnia"));
assertTrue(toString.contains("Series{"));
assertTrue(toString.contains("totalParts=0"));
assertTrue(toString.contains("isComplete=false"));
}
@Test
@DisplayName("Should pass validation with maximum allowed lengths")
void shouldPassValidationWithMaxAllowedLengths() {
String maxName = "a".repeat(255);
String maxDescription = "a".repeat(1000);
series.setName(maxName);
series.setDescription(maxDescription);
Set<ConstraintViolation<Series>> violations = validator.validate(series);
assertTrue(violations.isEmpty());
}
@Test
@DisplayName("Should update total parts when stories are added or removed")
void shouldUpdateTotalPartsWhenStoriesAreAddedOrRemoved() {
assertEquals(0, series.getTotalParts());
Story story1 = new Story("Part 1");
series.addStory(story1);
assertEquals(1, series.getTotalParts());
Story story2 = new Story("Part 2");
series.addStory(story2);
assertEquals(2, series.getTotalParts());
series.removeStory(story1);
assertEquals(1, series.getTotalParts());
}
}

View File

@@ -0,0 +1,250 @@
package com.storycove.entity;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Story Entity Tests")
class StoryTest {
private Validator validator;
private Story story;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
story = new Story("The Great Adventure");
}
@Test
@DisplayName("Should create story with valid title")
void shouldCreateStoryWithValidTitle() {
assertEquals("The Great Adventure", story.getTitle());
assertEquals(0, story.getWordCount());
assertEquals(0, story.getReadingTimeMinutes());
assertEquals(0.0, story.getAverageRating());
assertEquals(0, story.getTotalRatings());
assertFalse(story.getIsFavorite());
assertEquals(0.0, story.getReadingProgress());
assertNotNull(story.getTags());
assertTrue(story.getTags().isEmpty());
}
@Test
@DisplayName("Should create story with title and content")
void shouldCreateStoryWithTitleAndContent() {
String content = "<p>This is a test story with some content that has multiple words.</p>";
Story storyWithContent = new Story("Test Story", content);
assertEquals("Test Story", storyWithContent.getTitle());
assertEquals(content, storyWithContent.getContent());
assertTrue(storyWithContent.getWordCount() > 0);
assertTrue(storyWithContent.getReadingTimeMinutes() > 0);
}
@Test
@DisplayName("Should fail validation when title is blank")
void shouldFailValidationWhenTitleIsBlank() {
story.setTitle("");
Set<ConstraintViolation<Story>> violations = validator.validate(story);
assertEquals(1, violations.size());
assertEquals("Story title is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when title is null")
void shouldFailValidationWhenTitleIsNull() {
story.setTitle(null);
Set<ConstraintViolation<Story>> violations = validator.validate(story);
assertEquals(1, violations.size());
assertEquals("Story title is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when title exceeds 255 characters")
void shouldFailValidationWhenTitleTooLong() {
String longTitle = "a".repeat(256);
story.setTitle(longTitle);
Set<ConstraintViolation<Story>> violations = validator.validate(story);
assertEquals(1, violations.size());
assertEquals("Story title must not exceed 255 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when description exceeds 1000 characters")
void shouldFailValidationWhenDescriptionTooLong() {
String longDescription = "a".repeat(1001);
story.setDescription(longDescription);
Set<ConstraintViolation<Story>> violations = validator.validate(story);
assertEquals(1, violations.size());
assertEquals("Story description must not exceed 1000 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should update word count when content is set")
void shouldUpdateWordCountWhenContentIsSet() {
String htmlContent = "<p>This is a test story with <b>bold</b> text and <i>italic</i> text.</p>";
story.setContent(htmlContent);
// HTML tags should be stripped for word count
assertTrue(story.getWordCount() > 0);
assertEquals(13, story.getWordCount()); // "This is a test story with bold text and italic text."
assertEquals(1, story.getReadingTimeMinutes()); // 13 words / 200 = 0.065, rounded up to 1
}
@Test
@DisplayName("Should calculate reading time correctly")
void shouldCalculateReadingTimeCorrectly() {
// 300 words should take 2 minutes (300/200 = 1.5, rounded up to 2)
String content = String.join(" ", java.util.Collections.nCopies(300, "word"));
story.setContent(content);
assertEquals(300, story.getWordCount());
assertEquals(2, story.getReadingTimeMinutes());
}
@Test
@DisplayName("Should add and remove tags correctly")
void shouldAddAndRemoveTagsCorrectly() {
Tag tag1 = new Tag("fantasy");
Tag tag2 = new Tag("adventure");
story.addTag(tag1);
story.addTag(tag2);
assertEquals(2, story.getTags().size());
assertTrue(story.getTags().contains(tag1));
assertTrue(story.getTags().contains(tag2));
assertTrue(tag1.getStories().contains(story));
assertTrue(tag2.getStories().contains(story));
assertEquals(1, tag1.getUsageCount());
assertEquals(1, tag2.getUsageCount());
story.removeTag(tag1);
assertEquals(1, story.getTags().size());
assertFalse(story.getTags().contains(tag1));
assertFalse(tag1.getStories().contains(story));
assertEquals(0, tag1.getUsageCount());
}
@Test
@DisplayName("Should update rating correctly")
void shouldUpdateRatingCorrectly() {
story.updateRating(4.0);
assertEquals(4.0, story.getAverageRating());
assertEquals(1, story.getTotalRatings());
story.updateRating(5.0);
assertEquals(4.5, story.getAverageRating());
assertEquals(2, story.getTotalRatings());
story.updateRating(3.0);
assertEquals(4.0, story.getAverageRating());
assertEquals(3, story.getTotalRatings());
}
@Test
@DisplayName("Should update reading progress correctly")
void shouldUpdateReadingProgressCorrectly() {
LocalDateTime beforeUpdate = LocalDateTime.now();
story.updateReadingProgress(0.5);
assertEquals(0.5, story.getReadingProgress());
assertNotNull(story.getLastReadAt());
assertTrue(story.getLastReadAt().isAfter(beforeUpdate) || story.getLastReadAt().isEqual(beforeUpdate));
// Progress should be clamped between 0 and 1
story.updateReadingProgress(1.5);
assertEquals(1.0, story.getReadingProgress());
story.updateReadingProgress(-0.5);
assertEquals(0.0, story.getReadingProgress());
}
@Test
@DisplayName("Should check if story is part of series correctly")
void shouldCheckIfStoryIsPartOfSeriesCorrectly() {
assertFalse(story.isPartOfSeries());
Series series = new Series("Test Series");
story.setSeries(series);
assertFalse(story.isPartOfSeries()); // Still false because no part number
story.setPartNumber(1);
assertTrue(story.isPartOfSeries());
story.setSeries(null);
assertFalse(story.isPartOfSeries()); // False because no series
}
@Test
@DisplayName("Should handle equals and hashCode correctly")
void shouldHandleEqualsAndHashCodeCorrectly() {
Story story1 = new Story("Story 1");
Story story2 = new Story("Story 2");
Story story3 = new Story("Story 1");
assertNotEquals(story1, story2);
assertNotEquals(story1, story3); // Different because IDs are null
assertEquals(story1, story1);
assertNotEquals(story1, null);
assertNotEquals(story1, "not a story");
assertEquals(story1.hashCode(), story1.hashCode());
}
@Test
@DisplayName("Should have proper toString representation")
void shouldHaveProperToStringRepresentation() {
String toString = story.toString();
assertTrue(toString.contains("The Great Adventure"));
assertTrue(toString.contains("Story{"));
assertTrue(toString.contains("wordCount=0"));
assertTrue(toString.contains("averageRating=0.0"));
}
@Test
@DisplayName("Should pass validation with maximum allowed lengths")
void shouldPassValidationWithMaxAllowedLengths() {
String maxTitle = "a".repeat(255);
String maxDescription = "a".repeat(1000);
story.setTitle(maxTitle);
story.setDescription(maxDescription);
Set<ConstraintViolation<Story>> violations = validator.validate(story);
assertTrue(violations.isEmpty());
}
@Test
@DisplayName("Should handle empty content gracefully")
void shouldHandleEmptyContentGracefully() {
story.setContent("");
assertEquals(0, story.getWordCount());
assertEquals(1, story.getReadingTimeMinutes()); // Minimum 1 minute
story.setContent(null);
assertEquals(0, story.getWordCount());
assertEquals(0, story.getReadingTimeMinutes());
}
@Test
@DisplayName("Should handle HTML content correctly")
void shouldHandleHtmlContentCorrectly() {
String htmlContent = "<div><p>Hello <span>world</span>!</p><br/><p>This is a test.</p></div>";
story.setContent(htmlContent);
// Should count words after stripping HTML: "Hello world! This is a test."
assertEquals(6, story.getWordCount());
}
}

View File

@@ -0,0 +1,151 @@
package com.storycove.entity;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Tag Entity Tests")
class TagTest {
private Validator validator;
private Tag tag;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
tag = new Tag("sci-fi");
}
@Test
@DisplayName("Should create tag with valid name")
void shouldCreateTagWithValidName() {
assertEquals("sci-fi", tag.getName());
assertEquals(0, tag.getUsageCount());
assertNotNull(tag.getStories());
assertTrue(tag.getStories().isEmpty());
}
@Test
@DisplayName("Should create tag with name and description")
void shouldCreateTagWithNameAndDescription() {
Tag tagWithDesc = new Tag("fantasy", "Fantasy stories with magic and adventure");
assertEquals("fantasy", tagWithDesc.getName());
assertEquals("Fantasy stories with magic and adventure", tagWithDesc.getDescription());
}
@Test
@DisplayName("Should fail validation when name is blank")
void shouldFailValidationWhenNameIsBlank() {
tag.setName("");
Set<ConstraintViolation<Tag>> violations = validator.validate(tag);
assertEquals(1, violations.size());
assertEquals("Tag name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name is null")
void shouldFailValidationWhenNameIsNull() {
tag.setName(null);
Set<ConstraintViolation<Tag>> violations = validator.validate(tag);
assertEquals(1, violations.size());
assertEquals("Tag name is required", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when name exceeds 50 characters")
void shouldFailValidationWhenNameTooLong() {
String longName = "a".repeat(51);
tag.setName(longName);
Set<ConstraintViolation<Tag>> violations = validator.validate(tag);
assertEquals(1, violations.size());
assertEquals("Tag name must not exceed 50 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should fail validation when description exceeds 255 characters")
void shouldFailValidationWhenDescriptionTooLong() {
String longDescription = "a".repeat(256);
tag.setDescription(longDescription);
Set<ConstraintViolation<Tag>> violations = validator.validate(tag);
assertEquals(1, violations.size());
assertEquals("Tag description must not exceed 255 characters", violations.iterator().next().getMessage());
}
@Test
@DisplayName("Should increment usage count correctly")
void shouldIncrementUsageCountCorrectly() {
assertEquals(0, tag.getUsageCount());
tag.incrementUsage();
assertEquals(1, tag.getUsageCount());
tag.incrementUsage();
assertEquals(2, tag.getUsageCount());
}
@Test
@DisplayName("Should decrement usage count correctly")
void shouldDecrementUsageCountCorrectly() {
tag.setUsageCount(3);
tag.decrementUsage();
assertEquals(2, tag.getUsageCount());
tag.decrementUsage();
assertEquals(1, tag.getUsageCount());
tag.decrementUsage();
assertEquals(0, tag.getUsageCount());
// Should not go below 0
tag.decrementUsage();
assertEquals(0, tag.getUsageCount());
}
@Test
@DisplayName("Should handle equals and hashCode correctly")
void shouldHandleEqualsAndHashCodeCorrectly() {
Tag tag1 = new Tag("action");
Tag tag2 = new Tag("romance");
Tag tag3 = new Tag("action");
assertNotEquals(tag1, tag2);
assertNotEquals(tag1, tag3); // Different because IDs are null
assertEquals(tag1, tag1);
assertNotEquals(tag1, null);
assertNotEquals(tag1, "not a tag");
assertEquals(tag1.hashCode(), tag1.hashCode());
}
@Test
@DisplayName("Should have proper toString representation")
void shouldHaveProperToStringRepresentation() {
String toString = tag.toString();
assertTrue(toString.contains("sci-fi"));
assertTrue(toString.contains("Tag{"));
assertTrue(toString.contains("usageCount=0"));
}
@Test
@DisplayName("Should pass validation with maximum allowed lengths")
void shouldPassValidationWithMaxAllowedLengths() {
String maxName = "a".repeat(50);
String maxDescription = "a".repeat(255);
tag.setName(maxName);
tag.setDescription(maxDescription);
Set<ConstraintViolation<Tag>> violations = validator.validate(tag);
assertTrue(violations.isEmpty());
}
}

View File

@@ -0,0 +1,223 @@
package com.storycove.repository;
import com.storycove.entity.Author;
import com.storycove.entity.Story;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Author Repository Integration Tests")
class AuthorRepositoryTest extends BaseRepositoryTest {
@Autowired
private AuthorRepository authorRepository;
@Autowired
private StoryRepository storyRepository;
private Author author1;
private Author author2;
private Author author3;
@BeforeEach
void setUp() {
authorRepository.deleteAll();
storyRepository.deleteAll();
author1 = new Author("J.R.R. Tolkien");
author1.setBio("Author of The Lord of the Rings");
author1.addUrl("https://en.wikipedia.org/wiki/J._R._R._Tolkien");
author2 = new Author("George Orwell");
author2.setBio("Author of 1984 and Animal Farm");
author3 = new Author("Jane Austen");
author3.setBio("Author of Pride and Prejudice");
authorRepository.saveAll(List.of(author1, author2, author3));
}
@Test
@DisplayName("Should find author by name")
void shouldFindAuthorByName() {
Optional<Author> found = authorRepository.findByName("J.R.R. Tolkien");
assertTrue(found.isPresent());
assertEquals("J.R.R. Tolkien", found.get().getName());
}
@Test
@DisplayName("Should check if author exists by name")
void shouldCheckIfAuthorExistsByName() {
assertTrue(authorRepository.existsByName("George Orwell"));
assertFalse(authorRepository.existsByName("Unknown Author"));
}
@Test
@DisplayName("Should find authors by name containing ignoring case")
void shouldFindAuthorsByNameContainingIgnoreCase() {
List<Author> authors = authorRepository.findByNameContainingIgnoreCase("george");
assertEquals(1, authors.size());
assertEquals("George Orwell", authors.get(0).getName());
authors = authorRepository.findByNameContainingIgnoreCase("j");
assertEquals(2, authors.size()); // J.R.R. Tolkien and Jane Austen
}
@Test
@DisplayName("Should find authors by name containing with pagination")
void shouldFindAuthorsByNameContainingWithPagination() {
Page<Author> page = authorRepository.findByNameContainingIgnoreCase("", PageRequest.of(0, 2));
assertEquals(2, page.getContent().size());
assertEquals(3, page.getTotalElements());
assertTrue(page.hasNext());
}
@Test
@DisplayName("Should find authors with stories")
void shouldFindAuthorsWithStories() {
// Initially no authors have stories
List<Author> authorsWithStories = authorRepository.findAuthorsWithStories();
assertTrue(authorsWithStories.isEmpty());
// Add a story to author1
Story story = new Story("The Hobbit");
author1.addStory(story);
storyRepository.save(story);
authorRepository.save(author1);
authorsWithStories = authorRepository.findAuthorsWithStories();
assertEquals(1, authorsWithStories.size());
assertEquals("J.R.R. Tolkien", authorsWithStories.get(0).getName());
}
@Test
@DisplayName("Should find authors with stories with pagination")
void shouldFindAuthorsWithStoriesWithPagination() {
// Add stories to authors
Story story1 = new Story("The Hobbit");
Story story2 = new Story("1984");
author1.addStory(story1);
author2.addStory(story2);
storyRepository.saveAll(List.of(story1, story2));
authorRepository.saveAll(List.of(author1, author2));
Page<Author> page = authorRepository.findAuthorsWithStories(PageRequest.of(0, 1));
assertEquals(1, page.getContent().size());
assertEquals(2, page.getTotalElements());
}
@Test
@DisplayName("Should find top rated authors")
void shouldFindTopRatedAuthors() {
author1.setRating(4.5);
author2.setRating(4.8);
author3.setRating(4.2);
authorRepository.saveAll(List.of(author1, author2, author3));
List<Author> topRated = authorRepository.findTopRatedAuthors();
assertEquals(3, topRated.size());
assertEquals("George Orwell", topRated.get(0).getName()); // Highest rating first
assertEquals("J.R.R. Tolkien", topRated.get(1).getName());
assertEquals("Jane Austen", topRated.get(2).getName());
}
@Test
@DisplayName("Should find authors by minimum rating")
void shouldFindAuthorsByMinimumRating() {
author1.setRating(4.5);
author2.setRating(4.8);
author3.setRating(4.2);
authorRepository.saveAll(List.of(author1, author2, author3));
List<Author> authors = authorRepository.findAuthorsByMinimumRating(4.4);
assertEquals(2, authors.size());
assertEquals("George Orwell", authors.get(0).getName());
assertEquals("J.R.R. Tolkien", authors.get(1).getName());
}
@Test
@DisplayName("Should find most prolific authors")
void shouldFindMostProlificAuthors() {
// Add different numbers of stories to authors
Story story1 = new Story("The Hobbit");
Story story2 = new Story("LOTR 1");
Story story3 = new Story("LOTR 2");
Story story4 = new Story("1984");
author1.addStory(story1);
author1.addStory(story2);
author1.addStory(story3);
author2.addStory(story4);
storyRepository.saveAll(List.of(story1, story2, story3, story4));
authorRepository.saveAll(List.of(author1, author2));
List<Author> prolific = authorRepository.findMostProlificAuthors();
assertEquals(2, prolific.size());
assertEquals("J.R.R. Tolkien", prolific.get(0).getName()); // Has 3 stories
assertEquals("George Orwell", prolific.get(1).getName()); // Has 1 story
}
@Test
@DisplayName("Should find authors by URL domain")
void shouldFindAuthorsByUrlDomain() {
author1.addUrl("https://tolkiengateway.net/wiki/J.R.R._Tolkien");
author2.addUrl("https://www.orwellfoundation.com/");
authorRepository.saveAll(List.of(author1, author2));
List<Author> authors = authorRepository.findByUrlDomain("wikipedia");
assertEquals(1, authors.size());
assertEquals("J.R.R. Tolkien", authors.get(0).getName());
authors = authorRepository.findByUrlDomain("orwell");
assertEquals(1, authors.size());
assertEquals("George Orwell", authors.get(0).getName());
}
@Test
@DisplayName("Should count recent authors")
void shouldCountRecentAuthors() {
long count = authorRepository.countRecentAuthors(1);
assertEquals(3, count); // All authors are recent (created today)
count = authorRepository.countRecentAuthors(0);
assertEquals(0, count); // No authors created today (current date - 0 days)
}
@Test
@DisplayName("Should save and retrieve author with all properties")
void shouldSaveAndRetrieveAuthorWithAllProperties() {
Author author = new Author("Test Author");
author.setBio("Test bio");
author.setAvatarPath("/images/test-avatar.jpg");
author.setRating(4.5);
author.addUrl("https://example.com");
Author saved = authorRepository.save(author);
assertNotNull(saved.getId());
assertNotNull(saved.getCreatedAt());
assertNotNull(saved.getUpdatedAt());
Optional<Author> retrieved = authorRepository.findById(saved.getId());
assertTrue(retrieved.isPresent());
Author found = retrieved.get();
assertEquals("Test Author", found.getName());
assertEquals("Test bio", found.getBio());
assertEquals("/images/test-avatar.jpg", found.getAvatarPath());
assertEquals(4.5, found.getRating());
assertEquals(0.0, found.getAverageStoryRating()); // No stories, so 0.0
assertEquals(0, found.getTotalStoryRatings()); // No stories, so 0
assertEquals(1, found.getUrls().size());
assertTrue(found.getUrls().contains("https://example.com"));
}
}

View File

@@ -0,0 +1,29 @@
package com.storycove.repository;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public abstract class BaseRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("storycove_test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
}

View File

@@ -0,0 +1,318 @@
package com.storycove.repository;
import com.storycove.entity.Author;
import com.storycove.entity.Series;
import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Story Repository Integration Tests")
class StoryRepositoryTest extends BaseRepositoryTest {
@Autowired
private StoryRepository storyRepository;
@Autowired
private AuthorRepository authorRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private SeriesRepository seriesRepository;
private Author author;
private Series series;
private Tag tag1;
private Tag tag2;
private Story story1;
private Story story2;
private Story story3;
@BeforeEach
void setUp() {
storyRepository.deleteAll();
authorRepository.deleteAll();
tagRepository.deleteAll();
seriesRepository.deleteAll();
author = new Author("Test Author");
authorRepository.save(author);
series = new Series("Test Series");
seriesRepository.save(series);
tag1 = new Tag("fantasy");
tag2 = new Tag("adventure");
tagRepository.saveAll(List.of(tag1, tag2));
story1 = new Story("The Great Adventure");
story1.setDescription("An epic adventure story");
story1.setContent("<p>This is the content of the story with many words to test word count.</p>");
story1.setAuthor(author);
story1.addTag(tag1);
story1.addTag(tag2);
story1.setSourceUrl("https://example.com/story1");
story2 = new Story("The Sequel");
story2.setDescription("The sequel to the great adventure");
story2.setAuthor(author);
story2.setSeries(series);
story2.setPartNumber(1);
story2.addTag(tag1);
story2.setIsFavorite(true);
story3 = new Story("The Final Chapter");
story3.setDescription("The final chapter");
story3.setAuthor(author);
story3.setSeries(series);
story3.setPartNumber(2);
story3.updateReadingProgress(0.5);
storyRepository.saveAll(List.of(story1, story2, story3));
}
@Test
@DisplayName("Should find story by title")
void shouldFindStoryByTitle() {
Optional<Story> found = storyRepository.findByTitle("The Great Adventure");
assertTrue(found.isPresent());
assertEquals("The Great Adventure", found.get().getTitle());
}
@Test
@DisplayName("Should find stories by title containing ignoring case")
void shouldFindStoriesByTitleContainingIgnoreCase() {
List<Story> stories = storyRepository.findByTitleContainingIgnoreCase("great");
assertEquals(1, stories.size());
assertEquals("The Great Adventure", stories.get(0).getTitle());
stories = storyRepository.findByTitleContainingIgnoreCase("the");
assertEquals(3, stories.size());
}
@Test
@DisplayName("Should find stories by author")
void shouldFindStoriesByAuthor() {
List<Story> stories = storyRepository.findByAuthor(author);
assertEquals(3, stories.size());
Page<Story> page = storyRepository.findByAuthor(author, PageRequest.of(0, 2));
assertEquals(2, page.getContent().size());
assertEquals(3, page.getTotalElements());
}
@Test
@DisplayName("Should find stories by series")
void shouldFindStoriesBySeries() {
List<Story> stories = storyRepository.findBySeries(series);
assertEquals(2, stories.size());
List<Story> orderedStories = storyRepository.findBySeriesOrderByPartNumber(series.getId());
assertEquals(2, orderedStories.size());
assertEquals("The Sequel", orderedStories.get(0).getTitle()); // Part 1
assertEquals("The Final Chapter", orderedStories.get(1).getTitle()); // Part 2
}
@Test
@DisplayName("Should find story by series and part number")
void shouldFindStoryBySeriesAndPartNumber() {
Optional<Story> found = storyRepository.findBySeriesAndPartNumber(series.getId(), 1);
assertTrue(found.isPresent());
assertEquals("The Sequel", found.get().getTitle());
found = storyRepository.findBySeriesAndPartNumber(series.getId(), 99);
assertFalse(found.isPresent());
}
@Test
@DisplayName("Should find favorite stories")
void shouldFindFavoriteStories() {
List<Story> favorites = storyRepository.findByIsFavorite(true);
assertEquals(1, favorites.size());
assertEquals("The Sequel", favorites.get(0).getTitle());
Page<Story> page = storyRepository.findByIsFavorite(true, PageRequest.of(0, 10));
assertEquals(1, page.getContent().size());
}
@Test
@DisplayName("Should find stories by tag")
void shouldFindStoriesByTag() {
List<Story> fantasyStories = storyRepository.findByTag(tag1);
assertEquals(2, fantasyStories.size());
List<Story> adventureStories = storyRepository.findByTag(tag2);
assertEquals(1, adventureStories.size());
assertEquals("The Great Adventure", adventureStories.get(0).getTitle());
}
@Test
@DisplayName("Should find stories by tag names")
void shouldFindStoriesByTagNames() {
List<Story> stories = storyRepository.findByTagNames(List.of("fantasy"));
assertEquals(2, stories.size());
stories = storyRepository.findByTagNames(List.of("fantasy", "adventure"));
assertEquals(2, stories.size()); // Distinct stories with either tag
Page<Story> page = storyRepository.findByTagNames(List.of("fantasy"), PageRequest.of(0, 1));
assertEquals(1, page.getContent().size());
assertEquals(2, page.getTotalElements());
}
@Test
@DisplayName("Should find stories by minimum rating")
void shouldFindStoriesByMinimumRating() {
story1.setAverageRating(4.5);
story2.setAverageRating(4.8);
story3.setAverageRating(4.2);
storyRepository.saveAll(List.of(story1, story2, story3));
List<Story> stories = storyRepository.findByMinimumRating(4.4);
assertEquals(2, stories.size());
assertEquals("The Sequel", stories.get(0).getTitle()); // Highest rating first
assertEquals("The Great Adventure", stories.get(1).getTitle());
}
@Test
@DisplayName("Should find top rated stories")
void shouldFindTopRatedStories() {
story1.setAverageRating(4.5);
story2.setAverageRating(4.8);
story3.setAverageRating(4.2);
storyRepository.saveAll(List.of(story1, story2, story3));
List<Story> topRated = storyRepository.findTopRatedStories();
assertEquals(3, topRated.size());
assertEquals("The Sequel", topRated.get(0).getTitle());
assertEquals("The Great Adventure", topRated.get(1).getTitle());
assertEquals("The Final Chapter", topRated.get(2).getTitle());
}
@Test
@DisplayName("Should find stories by word count range")
void shouldFindStoriesByWordCountRange() {
// story1 has content, others don't
List<Story> stories = storyRepository.findByWordCountRange(1, 100);
assertEquals(1, stories.size());
assertEquals("The Great Adventure", stories.get(0).getTitle());
stories = storyRepository.findByWordCountRange(0, 0);
assertEquals(2, stories.size()); // story2 and story3 have 0 words
}
@Test
@DisplayName("Should find stories in progress")
void shouldFindStoriesInProgress() {
List<Story> inProgress = storyRepository.findStoriesInProgress();
assertEquals(1, inProgress.size());
assertEquals("The Final Chapter", inProgress.get(0).getTitle());
Page<Story> page = storyRepository.findStoriesInProgress(PageRequest.of(0, 10));
assertEquals(1, page.getContent().size());
}
@Test
@DisplayName("Should find completed stories")
void shouldFindCompletedStories() {
story1.updateReadingProgress(1.0);
storyRepository.save(story1);
List<Story> completed = storyRepository.findCompletedStories();
assertEquals(1, completed.size());
assertEquals("The Great Adventure", completed.get(0).getTitle());
}
@Test
@DisplayName("Should find recently read stories")
void shouldFindRecentlyRead() {
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
List<Story> recent = storyRepository.findRecentlyRead(oneHourAgo);
assertEquals(1, recent.size()); // Only story3 has been read (has lastReadAt set)
assertEquals("The Final Chapter", recent.get(0).getTitle());
}
@Test
@DisplayName("Should find recently added stories")
void shouldFindRecentlyAdded() {
List<Story> recent = storyRepository.findRecentlyAdded();
assertEquals(3, recent.size());
// Should be ordered by creation date descending
}
@Test
@DisplayName("Should find stories with source URL")
void shouldFindStoriesWithSourceUrl() {
List<Story> withSource = storyRepository.findStoriesWithSourceUrl();
assertEquals(1, withSource.size());
assertEquals("The Great Adventure", withSource.get(0).getTitle());
}
@Test
@DisplayName("Should check if story exists by source URL")
void shouldCheckIfStoryExistsBySourceUrl() {
assertTrue(storyRepository.existsBySourceUrl("https://example.com/story1"));
assertFalse(storyRepository.existsBySourceUrl("https://example.com/nonexistent"));
}
@Test
@DisplayName("Should find story by source URL")
void shouldFindStoryBySourceUrl() {
Optional<Story> found = storyRepository.findBySourceUrl("https://example.com/story1");
assertTrue(found.isPresent());
assertEquals("The Great Adventure", found.get().getTitle());
}
@Test
@DisplayName("Should count stories created since date")
void shouldCountStoriesCreatedSince() {
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
long count = storyRepository.countStoriesCreatedSince(oneHourAgo);
assertEquals(3, count); // All stories were created recently
}
@Test
@DisplayName("Should calculate average statistics")
void shouldCalculateAverageStatistics() {
Double avgWordCount = storyRepository.findAverageWordCount();
assertNotNull(avgWordCount);
assertTrue(avgWordCount >= 0);
story1.setAverageRating(4.0);
story1.setTotalRatings(1);
story2.setAverageRating(5.0);
story2.setTotalRatings(1);
storyRepository.saveAll(List.of(story1, story2));
Double avgRating = storyRepository.findOverallAverageRating();
assertNotNull(avgRating);
assertEquals(4.5, avgRating);
Long totalWords = storyRepository.findTotalWordCount();
assertNotNull(totalWords);
assertTrue(totalWords >= 0);
}
@Test
@DisplayName("Should find stories by keyword")
void shouldFindStoriesByKeyword() {
List<Story> stories = storyRepository.findByKeyword("adventure");
assertEquals(2, stories.size()); // Title and description matches
stories = storyRepository.findByKeyword("epic");
assertEquals(1, stories.size());
assertEquals("The Great Adventure", stories.get(0).getTitle());
}
}

View File

@@ -0,0 +1,310 @@
package com.storycove.service;
import com.storycove.entity.Author;
import com.storycove.repository.AuthorRepository;
import com.storycove.service.exception.DuplicateResourceException;
import com.storycove.service.exception.ResourceNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("Author Service Unit Tests")
class AuthorServiceTest {
@Mock
private AuthorRepository authorRepository;
@InjectMocks
private AuthorService authorService;
private Author testAuthor;
private UUID testId;
@BeforeEach
void setUp() {
testId = UUID.randomUUID();
testAuthor = new Author("Test Author");
testAuthor.setId(testId);
testAuthor.setBio("Test biography");
}
@Test
@DisplayName("Should find all authors")
void shouldFindAllAuthors() {
List<Author> authors = List.of(testAuthor, new Author("Another Author"));
when(authorRepository.findAll()).thenReturn(authors);
List<Author> result = authorService.findAll();
assertEquals(2, result.size());
verify(authorRepository).findAll();
}
@Test
@DisplayName("Should find all authors with pagination")
void shouldFindAllAuthorsWithPagination() {
Pageable pageable = PageRequest.of(0, 10);
Page<Author> page = new PageImpl<>(List.of(testAuthor));
when(authorRepository.findAll(pageable)).thenReturn(page);
Page<Author> result = authorService.findAll(pageable);
assertEquals(1, result.getContent().size());
verify(authorRepository).findAll(pageable);
}
@Test
@DisplayName("Should find author by ID")
void shouldFindAuthorById() {
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
Author result = authorService.findById(testId);
assertEquals(testAuthor.getName(), result.getName());
verify(authorRepository).findById(testId);
}
@Test
@DisplayName("Should throw exception when author not found by ID")
void shouldThrowExceptionWhenAuthorNotFoundById() {
when(authorRepository.findById(testId)).thenReturn(Optional.empty());
assertThrows(ResourceNotFoundException.class, () -> authorService.findById(testId));
verify(authorRepository).findById(testId);
}
@Test
@DisplayName("Should find author by name")
void shouldFindAuthorByName() {
when(authorRepository.findByName("Test Author")).thenReturn(Optional.of(testAuthor));
Author result = authorService.findByName("Test Author");
assertEquals(testAuthor.getName(), result.getName());
verify(authorRepository).findByName("Test Author");
}
@Test
@DisplayName("Should throw exception when author not found by name")
void shouldThrowExceptionWhenAuthorNotFoundByName() {
when(authorRepository.findByName("Unknown Author")).thenReturn(Optional.empty());
assertThrows(ResourceNotFoundException.class, () -> authorService.findByName("Unknown Author"));
verify(authorRepository).findByName("Unknown Author");
}
@Test
@DisplayName("Should search authors by name")
void shouldSearchAuthorsByName() {
List<Author> authors = List.of(testAuthor);
when(authorRepository.findByNameContainingIgnoreCase("test")).thenReturn(authors);
List<Author> result = authorService.searchByName("test");
assertEquals(1, result.size());
assertEquals(testAuthor.getName(), result.get(0).getName());
verify(authorRepository).findByNameContainingIgnoreCase("test");
}
@Test
@DisplayName("Should check if author exists by name")
void shouldCheckIfAuthorExistsByName() {
when(authorRepository.existsByName("Test Author")).thenReturn(true);
when(authorRepository.existsByName("Unknown Author")).thenReturn(false);
assertTrue(authorService.existsByName("Test Author"));
assertFalse(authorService.existsByName("Unknown Author"));
verify(authorRepository).existsByName("Test Author");
verify(authorRepository).existsByName("Unknown Author");
}
@Test
@DisplayName("Should create new author")
void shouldCreateNewAuthor() {
Author newAuthor = new Author("New Author");
when(authorRepository.existsByName("New Author")).thenReturn(false);
when(authorRepository.save(any(Author.class))).thenReturn(newAuthor);
Author result = authorService.create(newAuthor);
assertEquals("New Author", result.getName());
verify(authorRepository).existsByName("New Author");
verify(authorRepository).save(newAuthor);
}
@Test
@DisplayName("Should throw exception when creating duplicate author")
void shouldThrowExceptionWhenCreatingDuplicateAuthor() {
Author duplicateAuthor = new Author("Test Author");
when(authorRepository.existsByName("Test Author")).thenReturn(true);
assertThrows(DuplicateResourceException.class, () -> authorService.create(duplicateAuthor));
verify(authorRepository).existsByName("Test Author");
verify(authorRepository, never()).save(any());
}
@Test
@DisplayName("Should update existing author")
void shouldUpdateExistingAuthor() {
Author updates = new Author("Updated Author");
updates.setBio("Updated bio");
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.existsByName("Updated Author")).thenReturn(false);
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.update(testId, updates);
assertEquals("Updated Author", testAuthor.getName());
assertEquals("Updated bio", testAuthor.getBio());
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should throw exception when updating to duplicate name")
void shouldThrowExceptionWhenUpdatingToDuplicateName() {
Author updates = new Author("Existing Author");
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.existsByName("Existing Author")).thenReturn(true);
assertThrows(DuplicateResourceException.class, () -> authorService.update(testId, updates));
verify(authorRepository).findById(testId);
verify(authorRepository).existsByName("Existing Author");
verify(authorRepository, never()).save(any());
}
@Test
@DisplayName("Should delete author without stories")
void shouldDeleteAuthorWithoutStories() {
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
authorService.delete(testId);
verify(authorRepository).findById(testId);
verify(authorRepository).delete(testAuthor);
}
@Test
@DisplayName("Should throw exception when deleting author with stories")
void shouldThrowExceptionWhenDeletingAuthorWithStories() {
testAuthor.getStories().add(new com.storycove.entity.Story("Test Story"));
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
assertThrows(IllegalStateException.class, () -> authorService.delete(testId));
verify(authorRepository).findById(testId);
verify(authorRepository, never()).delete(any());
}
@Test
@DisplayName("Should add URL to author")
void shouldAddUrlToAuthor() {
String url = "https://example.com/author";
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.addUrl(testId, url);
assertTrue(result.getUrls().contains(url));
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should remove URL from author")
void shouldRemoveUrlFromAuthor() {
String url = "https://example.com/author";
testAuthor.addUrl(url);
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.removeUrl(testId, url);
assertFalse(result.getUrls().contains(url));
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should set direct author rating")
void shouldSetDirectAuthorRating() {
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.setDirectRating(testId, 4.5);
assertEquals(4.5, result.getRating());
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should throw exception for invalid direct rating")
void shouldThrowExceptionForInvalidDirectRating() {
assertThrows(IllegalArgumentException.class, () -> authorService.setDirectRating(testId, -1.0));
assertThrows(IllegalArgumentException.class, () -> authorService.setDirectRating(testId, 6.0));
verify(authorRepository, never()).findById(any());
verify(authorRepository, never()).save(any());
}
@Test
@DisplayName("Should set author avatar")
void shouldSetAuthorAvatar() {
String avatarPath = "/images/avatar.jpg";
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.setAvatar(testId, avatarPath);
assertEquals(avatarPath, result.getAvatarPath());
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should remove author avatar")
void shouldRemoveAuthorAvatar() {
testAuthor.setAvatarPath("/images/old-avatar.jpg");
when(authorRepository.findById(testId)).thenReturn(Optional.of(testAuthor));
when(authorRepository.save(any(Author.class))).thenReturn(testAuthor);
Author result = authorService.removeAvatar(testId);
assertNull(result.getAvatarPath());
verify(authorRepository).findById(testId);
verify(authorRepository).save(testAuthor);
}
@Test
@DisplayName("Should count recent authors")
void shouldCountRecentAuthors() {
when(authorRepository.countRecentAuthors(7)).thenReturn(5L);
long count = authorService.countRecentAuthors(7);
assertEquals(5L, count);
verify(authorRepository).countRecentAuthors(7);
}
}