Backend implementation
This commit is contained in:
@@ -17,8 +17,21 @@
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<testcontainers.version>1.19.3</testcontainers.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
124
backend/src/main/java/com/storycove/dto/AuthorDto.java
Normal file
124
backend/src/main/java/com/storycove/dto/AuthorDto.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.storycove.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AuthorDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Author name is required")
|
||||
@Size(max = 255, message = "Author name must not exceed 255 characters")
|
||||
private String name;
|
||||
|
||||
@Size(max = 1000, message = "Bio must not exceed 1000 characters")
|
||||
private String bio;
|
||||
|
||||
private String avatarPath;
|
||||
private Double rating;
|
||||
private Double averageStoryRating;
|
||||
private Integer totalStoryRatings;
|
||||
private List<String> urls;
|
||||
private Integer storyCount;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public AuthorDto() {}
|
||||
|
||||
public AuthorDto(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public String getAvatarPath() {
|
||||
return avatarPath;
|
||||
}
|
||||
|
||||
public void setAvatarPath(String avatarPath) {
|
||||
this.avatarPath = avatarPath;
|
||||
}
|
||||
|
||||
public Double getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
public void setRating(Double rating) {
|
||||
this.rating = rating;
|
||||
}
|
||||
|
||||
public Double getAverageStoryRating() {
|
||||
return averageStoryRating;
|
||||
}
|
||||
|
||||
public void setAverageStoryRating(Double averageStoryRating) {
|
||||
this.averageStoryRating = averageStoryRating;
|
||||
}
|
||||
|
||||
public Integer getTotalStoryRatings() {
|
||||
return totalStoryRatings;
|
||||
}
|
||||
|
||||
public void setTotalStoryRatings(Integer totalStoryRatings) {
|
||||
this.totalStoryRatings = totalStoryRatings;
|
||||
}
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void setUrls(List<String> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public Integer getStoryCount() {
|
||||
return storyCount;
|
||||
}
|
||||
|
||||
public void setStoryCount(Integer storyCount) {
|
||||
this.storyCount = storyCount;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
217
backend/src/main/java/com/storycove/dto/StoryDto.java
Normal file
217
backend/src/main/java/com/storycove/dto/StoryDto.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package com.storycove.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class StoryDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Story title is required")
|
||||
@Size(max = 255, message = "Story title must not exceed 255 characters")
|
||||
private String title;
|
||||
|
||||
@Size(max = 1000, message = "Story description must not exceed 1000 characters")
|
||||
private String description;
|
||||
|
||||
private String content;
|
||||
private String sourceUrl;
|
||||
private String coverPath;
|
||||
private Integer wordCount;
|
||||
private Integer readingTimeMinutes;
|
||||
private Double averageRating;
|
||||
private Integer totalRatings;
|
||||
private Boolean isFavorite;
|
||||
private Double readingProgress;
|
||||
private LocalDateTime lastReadAt;
|
||||
private Integer partNumber;
|
||||
|
||||
// Related entities as simple references
|
||||
private UUID authorId;
|
||||
private String authorName;
|
||||
private UUID seriesId;
|
||||
private String seriesName;
|
||||
private List<String> tagNames;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public StoryDto() {}
|
||||
|
||||
public StoryDto(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getSourceUrl() {
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
public void setSourceUrl(String sourceUrl) {
|
||||
this.sourceUrl = sourceUrl;
|
||||
}
|
||||
|
||||
public String getCoverPath() {
|
||||
return coverPath;
|
||||
}
|
||||
|
||||
public void setCoverPath(String coverPath) {
|
||||
this.coverPath = coverPath;
|
||||
}
|
||||
|
||||
public Integer getWordCount() {
|
||||
return wordCount;
|
||||
}
|
||||
|
||||
public void setWordCount(Integer wordCount) {
|
||||
this.wordCount = wordCount;
|
||||
}
|
||||
|
||||
public Integer getReadingTimeMinutes() {
|
||||
return readingTimeMinutes;
|
||||
}
|
||||
|
||||
public void setReadingTimeMinutes(Integer readingTimeMinutes) {
|
||||
this.readingTimeMinutes = readingTimeMinutes;
|
||||
}
|
||||
|
||||
public Double getAverageRating() {
|
||||
return averageRating;
|
||||
}
|
||||
|
||||
public void setAverageRating(Double averageRating) {
|
||||
this.averageRating = averageRating;
|
||||
}
|
||||
|
||||
public Integer getTotalRatings() {
|
||||
return totalRatings;
|
||||
}
|
||||
|
||||
public void setTotalRatings(Integer totalRatings) {
|
||||
this.totalRatings = totalRatings;
|
||||
}
|
||||
|
||||
public Boolean getIsFavorite() {
|
||||
return isFavorite;
|
||||
}
|
||||
|
||||
public void setIsFavorite(Boolean isFavorite) {
|
||||
this.isFavorite = isFavorite;
|
||||
}
|
||||
|
||||
public Double getReadingProgress() {
|
||||
return readingProgress;
|
||||
}
|
||||
|
||||
public void setReadingProgress(Double readingProgress) {
|
||||
this.readingProgress = readingProgress;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastReadAt() {
|
||||
return lastReadAt;
|
||||
}
|
||||
|
||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||
this.lastReadAt = lastReadAt;
|
||||
}
|
||||
|
||||
public Integer getPartNumber() {
|
||||
return partNumber;
|
||||
}
|
||||
|
||||
public void setPartNumber(Integer partNumber) {
|
||||
this.partNumber = partNumber;
|
||||
}
|
||||
|
||||
public UUID getAuthorId() {
|
||||
return authorId;
|
||||
}
|
||||
|
||||
public void setAuthorId(UUID authorId) {
|
||||
this.authorId = authorId;
|
||||
}
|
||||
|
||||
public String getAuthorName() {
|
||||
return authorName;
|
||||
}
|
||||
|
||||
public void setAuthorName(String authorName) {
|
||||
this.authorName = authorName;
|
||||
}
|
||||
|
||||
public UUID getSeriesId() {
|
||||
return seriesId;
|
||||
}
|
||||
|
||||
public void setSeriesId(UUID seriesId) {
|
||||
this.seriesId = seriesId;
|
||||
}
|
||||
|
||||
public String getSeriesName() {
|
||||
return seriesName;
|
||||
}
|
||||
|
||||
public void setSeriesName(String seriesName) {
|
||||
this.seriesName = seriesName;
|
||||
}
|
||||
|
||||
public List<String> getTagNames() {
|
||||
return tagNames;
|
||||
}
|
||||
|
||||
public void setTagNames(List<String> tagNames) {
|
||||
this.tagNames = tagNames;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
200
backend/src/main/java/com/storycove/entity/Author.java
Normal file
200
backend/src/main/java/com/storycove/entity/Author.java
Normal file
@@ -0,0 +1,200 @@
|
||||
package com.storycove.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "authors")
|
||||
public class Author {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Author name is required")
|
||||
@Size(max = 255, message = "Author name must not exceed 255 characters")
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Size(max = 1000, message = "Bio must not exceed 1000 characters")
|
||||
@Column(length = 1000)
|
||||
private String bio;
|
||||
|
||||
@Column(name = "avatar_path")
|
||||
private String avatarPath;
|
||||
|
||||
@Column(name = "rating")
|
||||
private Double rating = 0.0;
|
||||
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "author_urls", joinColumns = @JoinColumn(name = "author_id"))
|
||||
@Column(name = "url")
|
||||
private List<String> urls = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Story> stories = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Author() {}
|
||||
|
||||
public Author(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void addStory(Story story) {
|
||||
stories.add(story);
|
||||
story.setAuthor(this);
|
||||
}
|
||||
|
||||
public void removeStory(Story story) {
|
||||
stories.remove(story);
|
||||
story.setAuthor(null);
|
||||
}
|
||||
|
||||
public void addUrl(String url) {
|
||||
if (url != null && !urls.contains(url)) {
|
||||
urls.add(url);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUrl(String url) {
|
||||
urls.remove(url);
|
||||
}
|
||||
|
||||
public double getAverageStoryRating() {
|
||||
if (stories.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double totalRating = stories.stream()
|
||||
.filter(story -> story.getTotalRatings() > 0)
|
||||
.mapToDouble(story -> story.getAverageRating())
|
||||
.sum();
|
||||
|
||||
long ratedStoriesCount = stories.stream()
|
||||
.filter(story -> story.getTotalRatings() > 0)
|
||||
.count();
|
||||
|
||||
return ratedStoriesCount > 0 ? totalRating / ratedStoriesCount : 0.0;
|
||||
}
|
||||
|
||||
public int getTotalStoryRatings() {
|
||||
return stories.stream()
|
||||
.mapToInt(story -> story.getTotalRatings())
|
||||
.sum();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getBio() {
|
||||
return bio;
|
||||
}
|
||||
|
||||
public void setBio(String bio) {
|
||||
this.bio = bio;
|
||||
}
|
||||
|
||||
public String getAvatarPath() {
|
||||
return avatarPath;
|
||||
}
|
||||
|
||||
public void setAvatarPath(String avatarPath) {
|
||||
this.avatarPath = avatarPath;
|
||||
}
|
||||
|
||||
public Double getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
public void setRating(Double rating) {
|
||||
this.rating = rating;
|
||||
}
|
||||
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void setUrls(List<String> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public List<Story> getStories() {
|
||||
return stories;
|
||||
}
|
||||
|
||||
public void setStories(List<Story> stories) {
|
||||
this.stories = stories;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Author)) return false;
|
||||
Author author = (Author) o;
|
||||
return id != null && id.equals(author.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Author{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", rating=" + rating +
|
||||
", averageStoryRating=" + getAverageStoryRating() +
|
||||
", totalStoryRatings=" + getTotalStoryRatings() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
183
backend/src/main/java/com/storycove/entity/Series.java
Normal file
183
backend/src/main/java/com/storycove/entity/Series.java
Normal file
@@ -0,0 +1,183 @@
|
||||
package com.storycove.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "series")
|
||||
public class Series {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Series name is required")
|
||||
@Size(max = 255, message = "Series name must not exceed 255 characters")
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Size(max = 1000, message = "Series description must not exceed 1000 characters")
|
||||
@Column(length = 1000)
|
||||
private String description;
|
||||
|
||||
@Column(name = "total_parts")
|
||||
private Integer totalParts = 0;
|
||||
|
||||
@Column(name = "is_complete")
|
||||
private Boolean isComplete = false;
|
||||
|
||||
@OneToMany(mappedBy = "series", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@OrderBy("partNumber ASC")
|
||||
private List<Story> stories = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Series() {}
|
||||
|
||||
public Series(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Series(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void addStory(Story story) {
|
||||
stories.add(story);
|
||||
story.setSeries(this);
|
||||
updateTotalParts();
|
||||
}
|
||||
|
||||
public void removeStory(Story story) {
|
||||
stories.remove(story);
|
||||
story.setSeries(null);
|
||||
updateTotalParts();
|
||||
}
|
||||
|
||||
private void updateTotalParts() {
|
||||
this.totalParts = stories.size();
|
||||
}
|
||||
|
||||
public Story getNextStory(Story currentStory) {
|
||||
if (currentStory.getPartNumber() == null) return null;
|
||||
|
||||
return stories.stream()
|
||||
.filter(story -> story.getPartNumber() != null)
|
||||
.filter(story -> story.getPartNumber().equals(currentStory.getPartNumber() + 1))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Story getPreviousStory(Story currentStory) {
|
||||
if (currentStory.getPartNumber() == null || currentStory.getPartNumber() <= 1) return null;
|
||||
|
||||
return stories.stream()
|
||||
.filter(story -> story.getPartNumber() != null)
|
||||
.filter(story -> story.getPartNumber().equals(currentStory.getPartNumber() - 1))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Integer getTotalParts() {
|
||||
return totalParts;
|
||||
}
|
||||
|
||||
public void setTotalParts(Integer totalParts) {
|
||||
this.totalParts = totalParts;
|
||||
}
|
||||
|
||||
public Boolean getIsComplete() {
|
||||
return isComplete;
|
||||
}
|
||||
|
||||
public void setIsComplete(Boolean isComplete) {
|
||||
this.isComplete = isComplete;
|
||||
}
|
||||
|
||||
public List<Story> getStories() {
|
||||
return stories;
|
||||
}
|
||||
|
||||
public void setStories(List<Story> stories) {
|
||||
this.stories = stories;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Series)) return false;
|
||||
Series series = (Series) o;
|
||||
return id != null && id.equals(series.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Series{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", totalParts=" + totalParts +
|
||||
", isComplete=" + isComplete +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
317
backend/src/main/java/com/storycove/entity/Story.java
Normal file
317
backend/src/main/java/com/storycove/entity/Story.java
Normal file
@@ -0,0 +1,317 @@
|
||||
package com.storycove.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "stories")
|
||||
public class Story {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Story title is required")
|
||||
@Size(max = 255, message = "Story title must not exceed 255 characters")
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Size(max = 1000, message = "Story description must not exceed 1000 characters")
|
||||
@Column(length = 1000)
|
||||
private String description;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "source_url")
|
||||
private String sourceUrl;
|
||||
|
||||
@Column(name = "cover_path")
|
||||
private String coverPath;
|
||||
|
||||
@Column(name = "word_count")
|
||||
private Integer wordCount = 0;
|
||||
|
||||
@Column(name = "reading_time_minutes")
|
||||
private Integer readingTimeMinutes = 0;
|
||||
|
||||
@Column(name = "average_rating")
|
||||
private Double averageRating = 0.0;
|
||||
|
||||
@Column(name = "total_ratings")
|
||||
private Integer totalRatings = 0;
|
||||
|
||||
@Column(name = "is_favorite")
|
||||
private Boolean isFavorite = false;
|
||||
|
||||
@Column(name = "reading_progress")
|
||||
private Double readingProgress = 0.0;
|
||||
|
||||
@Column(name = "last_read_at")
|
||||
private LocalDateTime lastReadAt;
|
||||
|
||||
@Column(name = "part_number")
|
||||
private Integer partNumber;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "author_id")
|
||||
private Author author;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "series_id")
|
||||
private Series series;
|
||||
|
||||
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
||||
@JoinTable(
|
||||
name = "story_tags",
|
||||
joinColumns = @JoinColumn(name = "story_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "tag_id")
|
||||
)
|
||||
private Set<Tag> tags = new HashSet<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Story() {}
|
||||
|
||||
public Story(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Story(String title, String content) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
updateWordCount();
|
||||
}
|
||||
|
||||
public void addTag(Tag tag) {
|
||||
tags.add(tag);
|
||||
tag.getStories().add(this);
|
||||
tag.incrementUsage();
|
||||
}
|
||||
|
||||
public void removeTag(Tag tag) {
|
||||
tags.remove(tag);
|
||||
tag.getStories().remove(this);
|
||||
tag.decrementUsage();
|
||||
}
|
||||
|
||||
public void updateRating(double newRating) {
|
||||
if (totalRatings == 0) {
|
||||
averageRating = newRating;
|
||||
totalRatings = 1;
|
||||
} else {
|
||||
double totalScore = averageRating * totalRatings;
|
||||
totalRatings++;
|
||||
averageRating = (totalScore + newRating) / totalRatings;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateWordCount() {
|
||||
if (content != null) {
|
||||
String cleanText = content.replaceAll("<[^>]*>", "");
|
||||
String[] words = cleanText.trim().split("\\s+");
|
||||
this.wordCount = words.length;
|
||||
this.readingTimeMinutes = Math.max(1, (int) Math.ceil(wordCount / 200.0));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateReadingProgress(double progress) {
|
||||
this.readingProgress = Math.max(0.0, Math.min(1.0, progress));
|
||||
this.lastReadAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public boolean isPartOfSeries() {
|
||||
return series != null && partNumber != null;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
updateWordCount();
|
||||
}
|
||||
|
||||
public String getSourceUrl() {
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
public void setSourceUrl(String sourceUrl) {
|
||||
this.sourceUrl = sourceUrl;
|
||||
}
|
||||
|
||||
public String getCoverPath() {
|
||||
return coverPath;
|
||||
}
|
||||
|
||||
public void setCoverPath(String coverPath) {
|
||||
this.coverPath = coverPath;
|
||||
}
|
||||
|
||||
public Integer getWordCount() {
|
||||
return wordCount;
|
||||
}
|
||||
|
||||
public void setWordCount(Integer wordCount) {
|
||||
this.wordCount = wordCount;
|
||||
}
|
||||
|
||||
public Integer getReadingTimeMinutes() {
|
||||
return readingTimeMinutes;
|
||||
}
|
||||
|
||||
public void setReadingTimeMinutes(Integer readingTimeMinutes) {
|
||||
this.readingTimeMinutes = readingTimeMinutes;
|
||||
}
|
||||
|
||||
public Double getAverageRating() {
|
||||
return averageRating;
|
||||
}
|
||||
|
||||
public void setAverageRating(Double averageRating) {
|
||||
this.averageRating = averageRating;
|
||||
}
|
||||
|
||||
public Integer getTotalRatings() {
|
||||
return totalRatings;
|
||||
}
|
||||
|
||||
public void setTotalRatings(Integer totalRatings) {
|
||||
this.totalRatings = totalRatings;
|
||||
}
|
||||
|
||||
public Boolean getIsFavorite() {
|
||||
return isFavorite;
|
||||
}
|
||||
|
||||
public void setIsFavorite(Boolean isFavorite) {
|
||||
this.isFavorite = isFavorite;
|
||||
}
|
||||
|
||||
public Double getReadingProgress() {
|
||||
return readingProgress;
|
||||
}
|
||||
|
||||
public void setReadingProgress(Double readingProgress) {
|
||||
this.readingProgress = readingProgress;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastReadAt() {
|
||||
return lastReadAt;
|
||||
}
|
||||
|
||||
public void setLastReadAt(LocalDateTime lastReadAt) {
|
||||
this.lastReadAt = lastReadAt;
|
||||
}
|
||||
|
||||
public Integer getPartNumber() {
|
||||
return partNumber;
|
||||
}
|
||||
|
||||
public void setPartNumber(Integer partNumber) {
|
||||
this.partNumber = partNumber;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(Author author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public Series getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public void setSeries(Series series) {
|
||||
this.series = series;
|
||||
}
|
||||
|
||||
public Set<Tag> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(Set<Tag> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Story)) return false;
|
||||
Story story = (Story) o;
|
||||
return id != null && id.equals(story.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Story{" +
|
||||
"id=" + id +
|
||||
", title='" + title + '\'' +
|
||||
", wordCount=" + wordCount +
|
||||
", averageRating=" + averageRating +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
130
backend/src/main/java/com/storycove/entity/Tag.java
Normal file
130
backend/src/main/java/com/storycove/entity/Tag.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package com.storycove.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tags")
|
||||
public class Tag {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Tag name is required")
|
||||
@Size(max = 50, message = "Tag name must not exceed 50 characters")
|
||||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
@Size(max = 255, message = "Tag description must not exceed 255 characters")
|
||||
private String description;
|
||||
|
||||
@Column(name = "usage_count")
|
||||
private Integer usageCount = 0;
|
||||
|
||||
@ManyToMany(mappedBy = "tags")
|
||||
private Set<Story> stories = new HashSet<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Tag() {}
|
||||
|
||||
public Tag(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Tag(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void incrementUsage() {
|
||||
this.usageCount++;
|
||||
}
|
||||
|
||||
public void decrementUsage() {
|
||||
if (this.usageCount > 0) {
|
||||
this.usageCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Integer getUsageCount() {
|
||||
return usageCount;
|
||||
}
|
||||
|
||||
public void setUsageCount(Integer usageCount) {
|
||||
this.usageCount = usageCount;
|
||||
}
|
||||
|
||||
public Set<Story> getStories() {
|
||||
return stories;
|
||||
}
|
||||
|
||||
public void setStories(Set<Story> stories) {
|
||||
this.stories = stories;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Tag)) return false;
|
||||
Tag tag = (Tag) o;
|
||||
return id != null && id.equals(tag.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Tag{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
", usageCount=" + usageCount +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.storycove.repository;
|
||||
|
||||
import com.storycove.entity.Author;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface AuthorRepository extends JpaRepository<Author, UUID> {
|
||||
|
||||
Optional<Author> findByName(String name);
|
||||
|
||||
boolean existsByName(String name);
|
||||
|
||||
List<Author> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
Page<Author> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||
|
||||
@Query("SELECT a FROM Author a WHERE SIZE(a.stories) > 0")
|
||||
List<Author> findAuthorsWithStories();
|
||||
|
||||
@Query("SELECT a FROM Author a WHERE SIZE(a.stories) > 0")
|
||||
Page<Author> findAuthorsWithStories(Pageable pageable);
|
||||
|
||||
@Query("SELECT a FROM Author a ORDER BY a.rating DESC")
|
||||
List<Author> findTopRatedAuthors();
|
||||
|
||||
@Query("SELECT a FROM Author a WHERE a.rating >= :minRating ORDER BY a.rating DESC")
|
||||
List<Author> findAuthorsByMinimumRating(@Param("minRating") Double minRating);
|
||||
|
||||
@Query("SELECT a FROM Author a JOIN a.stories s GROUP BY a.id ORDER BY COUNT(s) DESC")
|
||||
List<Author> findMostProlificAuthors();
|
||||
|
||||
@Query("SELECT a FROM Author a JOIN a.stories s GROUP BY a.id ORDER BY COUNT(s) DESC")
|
||||
Page<Author> findMostProlificAuthors(Pageable pageable);
|
||||
|
||||
@Query("SELECT DISTINCT a FROM Author a JOIN a.urls u WHERE u LIKE %:domain%")
|
||||
List<Author> findByUrlDomain(@Param("domain") String domain);
|
||||
|
||||
@Query("SELECT COUNT(a) FROM Author a WHERE a.createdAt >= CURRENT_DATE - :days")
|
||||
long countRecentAuthors(@Param("days") int days);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.storycove.repository;
|
||||
|
||||
import com.storycove.entity.Series;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface SeriesRepository extends JpaRepository<Series, UUID> {
|
||||
|
||||
Optional<Series> findByName(String name);
|
||||
|
||||
boolean existsByName(String name);
|
||||
|
||||
List<Series> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
Page<Series> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Series s WHERE SIZE(s.stories) > 0")
|
||||
List<Series> findSeriesWithStories();
|
||||
|
||||
@Query("SELECT s FROM Series s WHERE SIZE(s.stories) > 0")
|
||||
Page<Series> findSeriesWithStories(Pageable pageable);
|
||||
|
||||
List<Series> findByIsComplete(Boolean isComplete);
|
||||
|
||||
Page<Series> findByIsComplete(Boolean isComplete, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Series s WHERE s.totalParts >= :minParts ORDER BY s.totalParts DESC")
|
||||
List<Series> findByMinimumParts(@Param("minParts") Integer minParts);
|
||||
|
||||
@Query("SELECT s FROM Series s ORDER BY s.totalParts DESC")
|
||||
List<Series> findLongestSeries();
|
||||
|
||||
@Query("SELECT s FROM Series s ORDER BY s.totalParts DESC")
|
||||
Page<Series> findLongestSeries(Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Series s WHERE SIZE(s.stories) = 0")
|
||||
List<Series> findEmptySeries();
|
||||
|
||||
@Query("SELECT s FROM Series s JOIN s.stories st GROUP BY s.id ORDER BY AVG(st.averageRating) DESC")
|
||||
List<Series> findTopRatedSeries();
|
||||
|
||||
@Query("SELECT s FROM Series s JOIN s.stories st GROUP BY s.id ORDER BY AVG(st.averageRating) DESC")
|
||||
Page<Series> findTopRatedSeries(Pageable pageable);
|
||||
|
||||
@Query("SELECT COUNT(s) FROM Series s WHERE s.createdAt >= CURRENT_DATE - :days")
|
||||
long countRecentSeries(@Param("days") int days);
|
||||
|
||||
@Query("SELECT s FROM Series s WHERE s.isComplete = false AND SIZE(s.stories) > 0 ORDER BY s.updatedAt DESC")
|
||||
List<Series> findIncompleteSeriesWithStories();
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
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.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface StoryRepository extends JpaRepository<Story, UUID> {
|
||||
|
||||
Optional<Story> findByTitle(String title);
|
||||
|
||||
List<Story> findByTitleContainingIgnoreCase(String title);
|
||||
|
||||
Page<Story> findByTitleContainingIgnoreCase(String title, Pageable pageable);
|
||||
|
||||
List<Story> findByAuthor(Author author);
|
||||
|
||||
Page<Story> findByAuthor(Author author, Pageable pageable);
|
||||
|
||||
List<Story> findBySeries(Series series);
|
||||
|
||||
Page<Story> findBySeries(Series series, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s JOIN s.series ser WHERE ser.id = :seriesId ORDER BY s.partNumber ASC")
|
||||
List<Story> findBySeriesOrderByPartNumber(@Param("seriesId") UUID seriesId);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.series.id = :seriesId AND s.partNumber = :partNumber")
|
||||
Optional<Story> findBySeriesAndPartNumber(@Param("seriesId") UUID seriesId, @Param("partNumber") Integer partNumber);
|
||||
|
||||
List<Story> findByIsFavorite(Boolean isFavorite);
|
||||
|
||||
Page<Story> findByIsFavorite(Boolean isFavorite, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s JOIN s.tags t WHERE t = :tag")
|
||||
List<Story> findByTag(@Param("tag") Tag tag);
|
||||
|
||||
@Query("SELECT s FROM Story s JOIN s.tags t WHERE t = :tag")
|
||||
Page<Story> findByTag(@Param("tag") Tag tag, Pageable pageable);
|
||||
|
||||
@Query("SELECT DISTINCT s FROM Story s JOIN s.tags t WHERE t.name IN :tagNames")
|
||||
List<Story> findByTagNames(@Param("tagNames") List<String> tagNames);
|
||||
|
||||
@Query("SELECT DISTINCT s FROM Story s JOIN s.tags t WHERE t.name IN :tagNames")
|
||||
Page<Story> findByTagNames(@Param("tagNames") List<String> tagNames, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.averageRating >= :minRating ORDER BY s.averageRating DESC")
|
||||
List<Story> findByMinimumRating(@Param("minRating") Double minRating);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.averageRating >= :minRating ORDER BY s.averageRating DESC")
|
||||
Page<Story> findByMinimumRating(@Param("minRating") Double minRating, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s ORDER BY s.averageRating DESC")
|
||||
List<Story> findTopRatedStories();
|
||||
|
||||
@Query("SELECT s FROM Story s ORDER BY s.averageRating DESC")
|
||||
Page<Story> findTopRatedStories(Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.wordCount BETWEEN :minWords AND :maxWords")
|
||||
List<Story> findByWordCountRange(@Param("minWords") Integer minWords, @Param("maxWords") Integer maxWords);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.wordCount BETWEEN :minWords AND :maxWords")
|
||||
Page<Story> findByWordCountRange(@Param("minWords") Integer minWords, @Param("maxWords") Integer maxWords, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.readingTimeMinutes BETWEEN :minTime AND :maxTime")
|
||||
List<Story> findByReadingTimeRange(@Param("minTime") Integer minTime, @Param("maxTime") Integer maxTime);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.readingProgress > 0 ORDER BY s.lastReadAt DESC")
|
||||
List<Story> findStoriesInProgress();
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.readingProgress > 0 ORDER BY s.lastReadAt DESC")
|
||||
Page<Story> findStoriesInProgress(Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.readingProgress >= 1.0 ORDER BY s.lastReadAt DESC")
|
||||
List<Story> findCompletedStories();
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.readingProgress >= 1.0 ORDER BY s.lastReadAt DESC")
|
||||
Page<Story> findCompletedStories(Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.lastReadAt >= :since ORDER BY s.lastReadAt DESC")
|
||||
List<Story> findRecentlyRead(@Param("since") LocalDateTime since);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.lastReadAt >= :since ORDER BY s.lastReadAt DESC")
|
||||
Page<Story> findRecentlyRead(@Param("since") LocalDateTime since, Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s ORDER BY s.createdAt DESC")
|
||||
List<Story> findRecentlyAdded();
|
||||
|
||||
@Query("SELECT s FROM Story s ORDER BY s.createdAt DESC")
|
||||
Page<Story> findRecentlyAdded(Pageable pageable);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.sourceUrl IS NOT NULL")
|
||||
List<Story> findStoriesWithSourceUrl();
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.coverPath IS NOT NULL")
|
||||
List<Story> findStoriesWithCover();
|
||||
|
||||
@Query("SELECT COUNT(s) FROM Story s WHERE s.createdAt >= :since")
|
||||
long countStoriesCreatedSince(@Param("since") LocalDateTime since);
|
||||
|
||||
@Query("SELECT AVG(s.wordCount) FROM Story s")
|
||||
Double findAverageWordCount();
|
||||
|
||||
@Query("SELECT AVG(s.averageRating) FROM Story s WHERE s.totalRatings > 0")
|
||||
Double findOverallAverageRating();
|
||||
|
||||
@Query("SELECT SUM(s.wordCount) FROM Story s")
|
||||
Long findTotalWordCount();
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.title LIKE %:keyword% OR s.description LIKE %:keyword%")
|
||||
List<Story> findByKeyword(@Param("keyword") String keyword);
|
||||
|
||||
@Query("SELECT s FROM Story s WHERE s.title LIKE %:keyword% OR s.description LIKE %:keyword%")
|
||||
Page<Story> findByKeyword(@Param("keyword") String keyword, Pageable pageable);
|
||||
|
||||
boolean existsBySourceUrl(String sourceUrl);
|
||||
|
||||
Optional<Story> findBySourceUrl(String sourceUrl);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.storycove.repository;
|
||||
|
||||
import com.storycove.entity.Tag;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface TagRepository extends JpaRepository<Tag, UUID> {
|
||||
|
||||
Optional<Tag> findByName(String name);
|
||||
|
||||
boolean existsByName(String name);
|
||||
|
||||
List<Tag> findByNameContainingIgnoreCase(String name);
|
||||
|
||||
Page<Tag> findByNameContainingIgnoreCase(String name, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE SIZE(t.stories) > 0 ORDER BY t.usageCount DESC")
|
||||
List<Tag> findUsedTags();
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE SIZE(t.stories) > 0 ORDER BY t.usageCount DESC")
|
||||
Page<Tag> findUsedTags(Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Tag t ORDER BY t.usageCount DESC")
|
||||
List<Tag> findMostUsedTags();
|
||||
|
||||
@Query("SELECT t FROM Tag t ORDER BY t.usageCount DESC")
|
||||
Page<Tag> findMostUsedTags(Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE t.usageCount >= :minUsage ORDER BY t.usageCount DESC")
|
||||
List<Tag> findTagsByMinimumUsage(@Param("minUsage") Integer minUsage);
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE SIZE(t.stories) = 0")
|
||||
List<Tag> findUnusedTags();
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE t.usageCount > :threshold ORDER BY t.usageCount DESC")
|
||||
List<Tag> findPopularTags(@Param("threshold") Integer threshold);
|
||||
|
||||
@Query("SELECT COUNT(t) FROM Tag t WHERE t.createdAt >= CURRENT_DATE - :days")
|
||||
long countRecentTags(@Param("days") int days);
|
||||
|
||||
@Query("SELECT t FROM Tag t WHERE t.name IN :names")
|
||||
List<Tag> findByNames(@Param("names") List<String> names);
|
||||
}
|
||||
201
backend/src/main/java/com/storycove/service/AuthorService.java
Normal file
201
backend/src/main/java/com/storycove/service/AuthorService.java
Normal file
@@ -0,0 +1,201 @@
|
||||
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 jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
public class AuthorService {
|
||||
|
||||
private final AuthorRepository authorRepository;
|
||||
|
||||
@Autowired
|
||||
public AuthorService(AuthorRepository authorRepository) {
|
||||
this.authorRepository = authorRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> findAll() {
|
||||
return authorRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Author> findAll(Pageable pageable) {
|
||||
return authorRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Author findById(UUID id) {
|
||||
return authorRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Author", id.toString()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Author> findByIdOptional(UUID id) {
|
||||
return authorRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Author findByName(String name) {
|
||||
return authorRepository.findByName(name)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Author", name));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Author> findByNameOptional(String name) {
|
||||
return authorRepository.findByName(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> searchByName(String name) {
|
||||
return authorRepository.findByNameContainingIgnoreCase(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Author> searchByName(String name, Pageable pageable) {
|
||||
return authorRepository.findByNameContainingIgnoreCase(name, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> findAuthorsWithStories() {
|
||||
return authorRepository.findAuthorsWithStories();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Author> findAuthorsWithStories(Pageable pageable) {
|
||||
return authorRepository.findAuthorsWithStories(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> findTopRatedAuthors() {
|
||||
return authorRepository.findTopRatedAuthors();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> findMostProlificAuthors() {
|
||||
return authorRepository.findMostProlificAuthors();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Author> findMostProlificAuthors(Pageable pageable) {
|
||||
return authorRepository.findMostProlificAuthors(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Author> findByUrlDomain(String domain) {
|
||||
return authorRepository.findByUrlDomain(domain);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsByName(String name) {
|
||||
return authorRepository.existsByName(name);
|
||||
}
|
||||
|
||||
public Author create(@Valid Author author) {
|
||||
validateAuthorForCreate(author);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
public Author update(UUID id, @Valid Author authorUpdates) {
|
||||
Author existingAuthor = findById(id);
|
||||
|
||||
// Check for name conflicts if name is being changed
|
||||
if (!existingAuthor.getName().equals(authorUpdates.getName()) &&
|
||||
existsByName(authorUpdates.getName())) {
|
||||
throw new DuplicateResourceException("Author", authorUpdates.getName());
|
||||
}
|
||||
|
||||
updateAuthorFields(existingAuthor, authorUpdates);
|
||||
return authorRepository.save(existingAuthor);
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
Author author = findById(id);
|
||||
|
||||
// Check if author has stories
|
||||
if (!author.getStories().isEmpty()) {
|
||||
throw new IllegalStateException("Cannot delete author with existing stories. Delete stories first or reassign them to another author.");
|
||||
}
|
||||
|
||||
authorRepository.delete(author);
|
||||
}
|
||||
|
||||
public Author addUrl(UUID id, String url) {
|
||||
Author author = findById(id);
|
||||
author.addUrl(url);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
public Author removeUrl(UUID id, String url) {
|
||||
Author author = findById(id);
|
||||
author.removeUrl(url);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
public Author setDirectRating(UUID id, double rating) {
|
||||
if (rating < 0 || rating > 5) {
|
||||
throw new IllegalArgumentException("Rating must be between 0 and 5");
|
||||
}
|
||||
|
||||
Author author = findById(id);
|
||||
author.setRating(rating);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
|
||||
public Author setAvatar(UUID id, String avatarPath) {
|
||||
Author author = findById(id);
|
||||
author.setAvatarPath(avatarPath);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
public Author removeAvatar(UUID id) {
|
||||
Author author = findById(id);
|
||||
author.setAvatarPath(null);
|
||||
return authorRepository.save(author);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long countRecentAuthors(int days) {
|
||||
return authorRepository.countRecentAuthors(days);
|
||||
}
|
||||
|
||||
private void validateAuthorForCreate(Author author) {
|
||||
if (existsByName(author.getName())) {
|
||||
throw new DuplicateResourceException("Author", author.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAuthorFields(Author existing, Author updates) {
|
||||
if (updates.getName() != null) {
|
||||
existing.setName(updates.getName());
|
||||
}
|
||||
if (updates.getBio() != null) {
|
||||
existing.setBio(updates.getBio());
|
||||
}
|
||||
if (updates.getAvatarPath() != null) {
|
||||
existing.setAvatarPath(updates.getAvatarPath());
|
||||
}
|
||||
if (updates.getRating() != null) {
|
||||
existing.setRating(updates.getRating());
|
||||
}
|
||||
if (updates.getUrls() != null && !updates.getUrls().isEmpty()) {
|
||||
existing.getUrls().clear();
|
||||
existing.getUrls().addAll(updates.getUrls());
|
||||
}
|
||||
}
|
||||
}
|
||||
237
backend/src/main/java/com/storycove/service/SeriesService.java
Normal file
237
backend/src/main/java/com/storycove/service/SeriesService.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package com.storycove.service;
|
||||
|
||||
import com.storycove.entity.Series;
|
||||
import com.storycove.repository.SeriesRepository;
|
||||
import com.storycove.service.exception.DuplicateResourceException;
|
||||
import com.storycove.service.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
public class SeriesService {
|
||||
|
||||
private final SeriesRepository seriesRepository;
|
||||
|
||||
@Autowired
|
||||
public SeriesService(SeriesRepository seriesRepository) {
|
||||
this.seriesRepository = seriesRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findAll() {
|
||||
return seriesRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findAll(Pageable pageable) {
|
||||
return seriesRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Series findById(UUID id) {
|
||||
return seriesRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Series", id.toString()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Series> findByIdOptional(UUID id) {
|
||||
return seriesRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Series findByName(String name) {
|
||||
return seriesRepository.findByName(name)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Series", name));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Series> findByNameOptional(String name) {
|
||||
return seriesRepository.findByName(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> searchByName(String name) {
|
||||
return seriesRepository.findByNameContainingIgnoreCase(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> searchByName(String name, Pageable pageable) {
|
||||
return seriesRepository.findByNameContainingIgnoreCase(name, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findSeriesWithStories() {
|
||||
return seriesRepository.findSeriesWithStories();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findSeriesWithStories(Pageable pageable) {
|
||||
return seriesRepository.findSeriesWithStories(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findCompleteSeries() {
|
||||
return seriesRepository.findByIsComplete(true);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findCompleteSeries(Pageable pageable) {
|
||||
return seriesRepository.findByIsComplete(true, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findIncompleteSeries() {
|
||||
return seriesRepository.findByIsComplete(false);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findIncompleteSeries(Pageable pageable) {
|
||||
return seriesRepository.findByIsComplete(false, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findIncompleteSeriesWithStories() {
|
||||
return seriesRepository.findIncompleteSeriesWithStories();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findLongestSeries() {
|
||||
return seriesRepository.findLongestSeries();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findLongestSeries(Pageable pageable) {
|
||||
return seriesRepository.findLongestSeries(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findTopRatedSeries() {
|
||||
return seriesRepository.findTopRatedSeries();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Series> findTopRatedSeries(Pageable pageable) {
|
||||
return seriesRepository.findTopRatedSeries(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findByMinimumParts(Integer minParts) {
|
||||
return seriesRepository.findByMinimumParts(minParts);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Series> findEmptySeries() {
|
||||
return seriesRepository.findEmptySeries();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsByName(String name) {
|
||||
return seriesRepository.existsByName(name);
|
||||
}
|
||||
|
||||
public Series create(@Valid Series series) {
|
||||
validateSeriesForCreate(series);
|
||||
return seriesRepository.save(series);
|
||||
}
|
||||
|
||||
public Series update(UUID id, @Valid Series seriesUpdates) {
|
||||
Series existingSeries = findById(id);
|
||||
|
||||
// Check for name conflicts if name is being changed
|
||||
if (!existingSeries.getName().equals(seriesUpdates.getName()) &&
|
||||
existsByName(seriesUpdates.getName())) {
|
||||
throw new DuplicateResourceException("Series", seriesUpdates.getName());
|
||||
}
|
||||
|
||||
updateSeriesFields(existingSeries, seriesUpdates);
|
||||
return seriesRepository.save(existingSeries);
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
Series series = findById(id);
|
||||
|
||||
// Check if series has stories
|
||||
if (!series.getStories().isEmpty()) {
|
||||
throw new IllegalStateException("Cannot delete series with existing stories. Delete stories first or remove them from the series.");
|
||||
}
|
||||
|
||||
seriesRepository.delete(series);
|
||||
}
|
||||
|
||||
public Series markComplete(UUID id) {
|
||||
Series series = findById(id);
|
||||
series.setIsComplete(true);
|
||||
return seriesRepository.save(series);
|
||||
}
|
||||
|
||||
public Series markIncomplete(UUID id) {
|
||||
Series series = findById(id);
|
||||
series.setIsComplete(false);
|
||||
return seriesRepository.save(series);
|
||||
}
|
||||
|
||||
public List<Series> deleteEmptySeries() {
|
||||
List<Series> emptySeries = findEmptySeries();
|
||||
seriesRepository.deleteAll(emptySeries);
|
||||
return emptySeries;
|
||||
}
|
||||
|
||||
public Series findOrCreate(String name) {
|
||||
return findByNameOptional(name)
|
||||
.orElseGet(() -> create(new Series(name)));
|
||||
}
|
||||
|
||||
public Series findOrCreate(String name, String description) {
|
||||
return findByNameOptional(name)
|
||||
.orElseGet(() -> create(new Series(name, description)));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long countRecentSeries(int days) {
|
||||
return seriesRepository.countRecentSeries(days);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getTotalCount() {
|
||||
return seriesRepository.count();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getActiveSeriesCount() {
|
||||
return findSeriesWithStories().size();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getCompleteSeriesCount() {
|
||||
return seriesRepository.findByIsComplete(true).size();
|
||||
}
|
||||
|
||||
private void validateSeriesForCreate(Series series) {
|
||||
if (existsByName(series.getName())) {
|
||||
throw new DuplicateResourceException("Series", series.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSeriesFields(Series existing, Series updates) {
|
||||
if (updates.getName() != null) {
|
||||
existing.setName(updates.getName());
|
||||
}
|
||||
if (updates.getDescription() != null) {
|
||||
existing.setDescription(updates.getDescription());
|
||||
}
|
||||
if (updates.getIsComplete() != null) {
|
||||
existing.setIsComplete(updates.getIsComplete());
|
||||
}
|
||||
}
|
||||
}
|
||||
423
backend/src/main/java/com/storycove/service/StoryService.java
Normal file
423
backend/src/main/java/com/storycove/service/StoryService.java
Normal file
@@ -0,0 +1,423 @@
|
||||
package com.storycove.service;
|
||||
|
||||
import com.storycove.entity.Author;
|
||||
import com.storycove.entity.Series;
|
||||
import com.storycove.entity.Story;
|
||||
import com.storycove.entity.Tag;
|
||||
import com.storycove.repository.StoryRepository;
|
||||
import com.storycove.service.exception.DuplicateResourceException;
|
||||
import com.storycove.service.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
public class StoryService {
|
||||
|
||||
private final StoryRepository storyRepository;
|
||||
private final AuthorService authorService;
|
||||
private final TagService tagService;
|
||||
private final SeriesService seriesService;
|
||||
|
||||
@Autowired
|
||||
public StoryService(StoryRepository storyRepository,
|
||||
AuthorService authorService,
|
||||
TagService tagService,
|
||||
SeriesService seriesService) {
|
||||
this.storyRepository = storyRepository;
|
||||
this.authorService = authorService;
|
||||
this.tagService = tagService;
|
||||
this.seriesService = seriesService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findAll() {
|
||||
return storyRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findAll(Pageable pageable) {
|
||||
return storyRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Story findById(UUID id) {
|
||||
return storyRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Story", id.toString()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findByIdOptional(UUID id) {
|
||||
return storyRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findByTitle(String title) {
|
||||
return storyRepository.findByTitle(title);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findBySourceUrl(String sourceUrl) {
|
||||
return storyRepository.findBySourceUrl(sourceUrl);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> searchByTitle(String title) {
|
||||
return storyRepository.findByTitleContainingIgnoreCase(title);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> searchByTitle(String title, Pageable pageable) {
|
||||
return storyRepository.findByTitleContainingIgnoreCase(title, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findByAuthor(UUID authorId) {
|
||||
Author author = authorService.findById(authorId);
|
||||
return storyRepository.findByAuthor(author);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findByAuthor(UUID authorId, Pageable pageable) {
|
||||
Author author = authorService.findById(authorId);
|
||||
return storyRepository.findByAuthor(author, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findBySeries(UUID seriesId) {
|
||||
Series series = seriesService.findById(seriesId);
|
||||
return storyRepository.findBySeriesOrderByPartNumber(seriesId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findBySeries(UUID seriesId, Pageable pageable) {
|
||||
Series series = seriesService.findById(seriesId);
|
||||
return storyRepository.findBySeries(series, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Story> findBySeriesAndPartNumber(UUID seriesId, Integer partNumber) {
|
||||
return storyRepository.findBySeriesAndPartNumber(seriesId, partNumber);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findByTag(UUID tagId) {
|
||||
Tag tag = tagService.findById(tagId);
|
||||
return storyRepository.findByTag(tag);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findByTag(UUID tagId, Pageable pageable) {
|
||||
Tag tag = tagService.findById(tagId);
|
||||
return storyRepository.findByTag(tag, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findByTagNames(List<String> tagNames) {
|
||||
return storyRepository.findByTagNames(tagNames);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findByTagNames(List<String> tagNames, Pageable pageable) {
|
||||
return storyRepository.findByTagNames(tagNames, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findFavorites() {
|
||||
return storyRepository.findByIsFavorite(true);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findFavorites(Pageable pageable) {
|
||||
return storyRepository.findByIsFavorite(true, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findStoriesInProgress() {
|
||||
return storyRepository.findStoriesInProgress();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findStoriesInProgress(Pageable pageable) {
|
||||
return storyRepository.findStoriesInProgress(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findCompletedStories() {
|
||||
return storyRepository.findCompletedStories();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findRecentlyRead(int hours) {
|
||||
LocalDateTime since = LocalDateTime.now().minusHours(hours);
|
||||
return storyRepository.findRecentlyRead(since);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findRecentlyAdded() {
|
||||
return storyRepository.findRecentlyAdded();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findRecentlyAdded(Pageable pageable) {
|
||||
return storyRepository.findRecentlyAdded(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findTopRated() {
|
||||
return storyRepository.findTopRatedStories();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> findTopRated(Pageable pageable) {
|
||||
return storyRepository.findTopRatedStories(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> findByWordCountRange(Integer minWords, Integer maxWords) {
|
||||
return storyRepository.findByWordCountRange(minWords, maxWords);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Story> searchByKeyword(String keyword) {
|
||||
return storyRepository.findByKeyword(keyword);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Story> searchByKeyword(String keyword, Pageable pageable) {
|
||||
return storyRepository.findByKeyword(keyword, pageable);
|
||||
}
|
||||
|
||||
public Story create(@Valid Story story) {
|
||||
validateStoryForCreate(story);
|
||||
|
||||
// Set up relationships
|
||||
if (story.getAuthor() != null && story.getAuthor().getId() != null) {
|
||||
Author author = authorService.findById(story.getAuthor().getId());
|
||||
story.setAuthor(author);
|
||||
}
|
||||
|
||||
if (story.getSeries() != null && story.getSeries().getId() != null) {
|
||||
Series series = seriesService.findById(story.getSeries().getId());
|
||||
story.setSeries(series);
|
||||
validateSeriesPartNumber(series, story.getPartNumber());
|
||||
}
|
||||
|
||||
Story savedStory = storyRepository.save(story);
|
||||
|
||||
// Handle tags
|
||||
if (story.getTags() != null && !story.getTags().isEmpty()) {
|
||||
updateStoryTags(savedStory, story.getTags());
|
||||
}
|
||||
|
||||
return savedStory;
|
||||
}
|
||||
|
||||
public Story update(UUID id, @Valid Story storyUpdates) {
|
||||
Story existingStory = findById(id);
|
||||
|
||||
// Check for source URL conflicts if URL is being changed
|
||||
if (storyUpdates.getSourceUrl() != null &&
|
||||
!storyUpdates.getSourceUrl().equals(existingStory.getSourceUrl()) &&
|
||||
storyRepository.existsBySourceUrl(storyUpdates.getSourceUrl())) {
|
||||
throw new DuplicateResourceException("Story with source URL", storyUpdates.getSourceUrl());
|
||||
}
|
||||
|
||||
updateStoryFields(existingStory, storyUpdates);
|
||||
return storyRepository.save(existingStory);
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
Story story = findById(id);
|
||||
|
||||
// Remove from series if part of one
|
||||
if (story.getSeries() != null) {
|
||||
story.getSeries().removeStory(story);
|
||||
}
|
||||
|
||||
// Remove tags (this will update tag usage counts)
|
||||
story.getTags().forEach(tag -> story.removeTag(tag));
|
||||
|
||||
storyRepository.delete(story);
|
||||
}
|
||||
|
||||
public Story addToFavorites(UUID id) {
|
||||
Story story = findById(id);
|
||||
story.setIsFavorite(true);
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story removeFromFavorites(UUID id) {
|
||||
Story story = findById(id);
|
||||
story.setIsFavorite(false);
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story updateReadingProgress(UUID id, double progress) {
|
||||
if (progress < 0 || progress > 1) {
|
||||
throw new IllegalArgumentException("Reading progress must be between 0 and 1");
|
||||
}
|
||||
|
||||
Story story = findById(id);
|
||||
story.updateReadingProgress(progress);
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story updateRating(UUID id, double rating) {
|
||||
if (rating < 0 || rating > 5) {
|
||||
throw new IllegalArgumentException("Rating must be between 0 and 5");
|
||||
}
|
||||
|
||||
Story story = findById(id);
|
||||
story.updateRating(rating);
|
||||
|
||||
// Note: Author's average story rating will be calculated dynamically
|
||||
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story setCover(UUID id, String coverPath) {
|
||||
Story story = findById(id);
|
||||
story.setCoverPath(coverPath);
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story removeCover(UUID id) {
|
||||
Story story = findById(id);
|
||||
story.setCoverPath(null);
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story addToSeries(UUID storyId, UUID seriesId, Integer partNumber) {
|
||||
Story story = findById(storyId);
|
||||
Series series = seriesService.findById(seriesId);
|
||||
|
||||
validateSeriesPartNumber(series, partNumber);
|
||||
|
||||
story.setSeries(series);
|
||||
story.setPartNumber(partNumber);
|
||||
series.addStory(story);
|
||||
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
public Story removeFromSeries(UUID storyId) {
|
||||
Story story = findById(storyId);
|
||||
|
||||
if (story.getSeries() != null) {
|
||||
story.getSeries().removeStory(story);
|
||||
story.setSeries(null);
|
||||
story.setPartNumber(null);
|
||||
}
|
||||
|
||||
return storyRepository.save(story);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsBySourceUrl(String sourceUrl) {
|
||||
return storyRepository.existsBySourceUrl(sourceUrl);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Double getAverageWordCount() {
|
||||
return storyRepository.findAverageWordCount();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Double getOverallAverageRating() {
|
||||
return storyRepository.findOverallAverageRating();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Long getTotalWordCount() {
|
||||
return storyRepository.findTotalWordCount();
|
||||
}
|
||||
|
||||
private void validateStoryForCreate(Story story) {
|
||||
if (story.getSourceUrl() != null && existsBySourceUrl(story.getSourceUrl())) {
|
||||
throw new DuplicateResourceException("Story with source URL", story.getSourceUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSeriesPartNumber(Series series, Integer partNumber) {
|
||||
if (partNumber != null) {
|
||||
Optional<Story> existingPart = storyRepository.findBySeriesAndPartNumber(series.getId(), partNumber);
|
||||
if (existingPart.isPresent()) {
|
||||
throw new DuplicateResourceException("Story", "part " + partNumber + " of series " + series.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStoryFields(Story existing, Story updates) {
|
||||
if (updates.getTitle() != null) {
|
||||
existing.setTitle(updates.getTitle());
|
||||
}
|
||||
if (updates.getDescription() != null) {
|
||||
existing.setDescription(updates.getDescription());
|
||||
}
|
||||
if (updates.getContent() != null) {
|
||||
existing.setContent(updates.getContent());
|
||||
}
|
||||
if (updates.getSourceUrl() != null) {
|
||||
existing.setSourceUrl(updates.getSourceUrl());
|
||||
}
|
||||
if (updates.getCoverPath() != null) {
|
||||
existing.setCoverPath(updates.getCoverPath());
|
||||
}
|
||||
if (updates.getIsFavorite() != null) {
|
||||
existing.setIsFavorite(updates.getIsFavorite());
|
||||
}
|
||||
|
||||
// Handle author update
|
||||
if (updates.getAuthor() != null && updates.getAuthor().getId() != null) {
|
||||
Author author = authorService.findById(updates.getAuthor().getId());
|
||||
existing.setAuthor(author);
|
||||
}
|
||||
|
||||
// Handle series update
|
||||
if (updates.getSeries() != null && updates.getSeries().getId() != null) {
|
||||
Series series = seriesService.findById(updates.getSeries().getId());
|
||||
existing.setSeries(series);
|
||||
if (updates.getPartNumber() != null) {
|
||||
validateSeriesPartNumber(series, updates.getPartNumber());
|
||||
existing.setPartNumber(updates.getPartNumber());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tags update
|
||||
if (updates.getTags() != null) {
|
||||
updateStoryTags(existing, updates.getTags());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStoryTags(Story story, Set<Tag> newTags) {
|
||||
// Remove existing tags
|
||||
story.getTags().forEach(tag -> story.removeTag(tag));
|
||||
story.getTags().clear();
|
||||
|
||||
// Add new tags
|
||||
for (Tag tag : newTags) {
|
||||
Tag managedTag;
|
||||
if (tag.getId() != null) {
|
||||
managedTag = tagService.findById(tag.getId());
|
||||
} else {
|
||||
// Try to find existing tag by name or create new one
|
||||
managedTag = tagService.findByNameOptional(tag.getName())
|
||||
.orElseGet(() -> tagService.create(tag));
|
||||
}
|
||||
story.addTag(managedTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
187
backend/src/main/java/com/storycove/service/TagService.java
Normal file
187
backend/src/main/java/com/storycove/service/TagService.java
Normal file
@@ -0,0 +1,187 @@
|
||||
package com.storycove.service;
|
||||
|
||||
import com.storycove.entity.Tag;
|
||||
import com.storycove.repository.TagRepository;
|
||||
import com.storycove.service.exception.DuplicateResourceException;
|
||||
import com.storycove.service.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@Transactional
|
||||
public class TagService {
|
||||
|
||||
private final TagRepository tagRepository;
|
||||
|
||||
@Autowired
|
||||
public TagService(TagRepository tagRepository) {
|
||||
this.tagRepository = tagRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findAll() {
|
||||
return tagRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Tag> findAll(Pageable pageable) {
|
||||
return tagRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Tag findById(UUID id) {
|
||||
return tagRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Tag", id.toString()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Tag> findByIdOptional(UUID id) {
|
||||
return tagRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Tag findByName(String name) {
|
||||
return tagRepository.findByName(name)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Tag", name));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Tag> findByNameOptional(String name) {
|
||||
return tagRepository.findByName(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> searchByName(String name) {
|
||||
return tagRepository.findByNameContainingIgnoreCase(name);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Tag> searchByName(String name, Pageable pageable) {
|
||||
return tagRepository.findByNameContainingIgnoreCase(name, pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findUsedTags() {
|
||||
return tagRepository.findUsedTags();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Tag> findUsedTags(Pageable pageable) {
|
||||
return tagRepository.findUsedTags(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findMostUsedTags() {
|
||||
return tagRepository.findMostUsedTags();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Tag> findMostUsedTags(Pageable pageable) {
|
||||
return tagRepository.findMostUsedTags(pageable);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findPopularTags(Integer threshold) {
|
||||
return tagRepository.findPopularTags(threshold);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findUnusedTags() {
|
||||
return tagRepository.findUnusedTags();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Tag> findByNames(List<String> names) {
|
||||
return tagRepository.findByNames(names);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean existsByName(String name) {
|
||||
return tagRepository.existsByName(name);
|
||||
}
|
||||
|
||||
public Tag create(@Valid Tag tag) {
|
||||
validateTagForCreate(tag);
|
||||
return tagRepository.save(tag);
|
||||
}
|
||||
|
||||
public Tag update(UUID id, @Valid Tag tagUpdates) {
|
||||
Tag existingTag = findById(id);
|
||||
|
||||
// Check for name conflicts if name is being changed
|
||||
if (!existingTag.getName().equals(tagUpdates.getName()) &&
|
||||
existsByName(tagUpdates.getName())) {
|
||||
throw new DuplicateResourceException("Tag", tagUpdates.getName());
|
||||
}
|
||||
|
||||
updateTagFields(existingTag, tagUpdates);
|
||||
return tagRepository.save(existingTag);
|
||||
}
|
||||
|
||||
public void delete(UUID id) {
|
||||
Tag tag = findById(id);
|
||||
|
||||
// Check if tag is used by any stories
|
||||
if (!tag.getStories().isEmpty()) {
|
||||
throw new IllegalStateException("Cannot delete tag that is used by stories. Remove tag from all stories first.");
|
||||
}
|
||||
|
||||
tagRepository.delete(tag);
|
||||
}
|
||||
|
||||
public List<Tag> deleteUnusedTags() {
|
||||
List<Tag> unusedTags = findUnusedTags();
|
||||
tagRepository.deleteAll(unusedTags);
|
||||
return unusedTags;
|
||||
}
|
||||
|
||||
public Tag findOrCreate(String name) {
|
||||
return findByNameOptional(name)
|
||||
.orElseGet(() -> create(new Tag(name)));
|
||||
}
|
||||
|
||||
public Tag findOrCreate(String name, String description) {
|
||||
return findByNameOptional(name)
|
||||
.orElseGet(() -> create(new Tag(name, description)));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long countRecentTags(int days) {
|
||||
return tagRepository.countRecentTags(days);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getTotalCount() {
|
||||
return tagRepository.count();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public long getUsedTagCount() {
|
||||
return findUsedTags().size();
|
||||
}
|
||||
|
||||
private void validateTagForCreate(Tag tag) {
|
||||
if (existsByName(tag.getName())) {
|
||||
throw new DuplicateResourceException("Tag", tag.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTagFields(Tag existing, Tag updates) {
|
||||
if (updates.getName() != null) {
|
||||
existing.setName(updates.getName());
|
||||
}
|
||||
if (updates.getDescription() != null) {
|
||||
existing.setDescription(updates.getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.storycove.service.exception;
|
||||
|
||||
public class DuplicateResourceException extends RuntimeException {
|
||||
|
||||
public DuplicateResourceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DuplicateResourceException(String resourceType, String identifier) {
|
||||
super(String.format("%s already exists with identifier: %s", resourceType, identifier));
|
||||
}
|
||||
|
||||
public DuplicateResourceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.storycove.service.exception;
|
||||
|
||||
public class ResourceNotFoundException extends RuntimeException {
|
||||
|
||||
public ResourceNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String resourceType, String identifier) {
|
||||
super(String.format("%s not found with identifier: %s", resourceType, identifier));
|
||||
}
|
||||
|
||||
public ResourceNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
190
backend/src/test/java/com/storycove/entity/AuthorTest.java
Normal file
190
backend/src/test/java/com/storycove/entity/AuthorTest.java
Normal 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{"));
|
||||
}
|
||||
}
|
||||
210
backend/src/test/java/com/storycove/entity/SeriesTest.java
Normal file
210
backend/src/test/java/com/storycove/entity/SeriesTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
250
backend/src/test/java/com/storycove/entity/StoryTest.java
Normal file
250
backend/src/test/java/com/storycove/entity/StoryTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
151
backend/src/test/java/com/storycove/entity/TagTest.java
Normal file
151
backend/src/test/java/com/storycove/entity/TagTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user