Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI Pipeline

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for Gradle wrapper
run: chmod +x ./gradlew

- name: Build project
run: ./gradlew build

- name: Run tests
run: ./gradlew test
8 changes: 6 additions & 2 deletions src/main/java/io/spring/api/ArticlesApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;
import org.springframework.validation.annotation.Validated;

@Validated
@RestController
@RequestMapping(path = "/articles")
@AllArgsConstructor
Expand Down Expand Up @@ -47,8 +51,8 @@ public ResponseEntity getFeed(

@GetMapping
public ResponseEntity getArticles(
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "20") int limit,
@RequestParam(value = "offset", defaultValue = "0") @Min(0) int offset,
@RequestParam(value = "limit", defaultValue = "20") @Min(1) @Max(50) int limit,
@RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "favorited", required = false) String favoritedBy,
@RequestParam(value = "author", required = false) String author,
Expand Down
30 changes: 15 additions & 15 deletions src/main/java/io/spring/api/UsersApi.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.spring.api;

import static org.springframework.web.bind.annotation.RequestMethod.POST;

import com.fasterxml.jackson.annotation.JsonRootName;
import io.spring.api.exception.InvalidAuthenticationException;
import io.spring.application.UserQueryService;
Expand All @@ -18,13 +16,16 @@
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -36,37 +37,35 @@ public class UsersApi {
private JwtService jwtService;
private UserService userService;

@RequestMapping(path = "/users", method = POST)
public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) {
@PostMapping("/users")
public ResponseEntity<Map<String, Object>> createUser(@Valid @RequestBody RegisterParam registerParam) {
User user = userService.createUser(registerParam);
UserData userData = userQueryService.findById(user.getId()).get();
return ResponseEntity.status(201)
return ResponseEntity.status(HttpStatus.CREATED)
.body(userResponse(new UserWithToken(userData, jwtService.toToken(user))));
}

@RequestMapping(path = "/users/login", method = POST)
public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) {
@PostMapping("/users/login")
public ResponseEntity<Map<String, Object>> userLogin(@Valid @RequestBody LoginParam loginParam) {
Optional<User> optional = userRepository.findByEmail(loginParam.getEmail());
if (optional.isPresent()
&& passwordEncoder.matches(loginParam.getPassword(), optional.get().getPassword())) {
UserData userData = userQueryService.findById(optional.get().getId()).get();
return ResponseEntity.ok(
userResponse(new UserWithToken(userData, jwtService.toToken(optional.get()))));
} else {
throw new InvalidAuthenticationException();
}
throw new InvalidAuthenticationException();
}

private Map<String, Object> userResponse(UserWithToken userWithToken) {
return new HashMap<String, Object>() {
{
put("user", userWithToken);
}
};
Map<String, Object> map = new HashMap<>();
map.put("user", userWithToken);
return map;
}
}

@Getter
@Setter
@JsonRootName("user")
@NoArgsConstructor
class LoginParam {
Expand All @@ -75,5 +74,6 @@ class LoginParam {
private String email;

@NotBlank(message = "can't be empty")
@Size(min = 8, max = 128, message = "length must be 8-128")
private String password;
}
14 changes: 14 additions & 0 deletions src/main/java/io/spring/application/TagsQueryService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.spring.application;

import io.spring.infrastructure.mybatis.readservice.TagReadService;
import java.util.Collections;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -13,4 +14,17 @@ public class TagsQueryService {
public List<String> allTags() {
return tagReadService.all();
}

public List<String> allTagsSorted(String sortBy) {
List<String> tags = tagReadService.all();

if ("name_desc".equals(sortBy)) {
Collections.sort(tags, Collections.reverseOrder());
} else if ("name_asc".equals(sortBy)) {
Collections.sort(tags);
} else {
Collections.sort(tags);

return tags;
}
}
3 changes: 3 additions & 0 deletions src/main/java/io/spring/application/user/RegisterParam.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonRootName;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -18,9 +19,11 @@ public class RegisterParam {
private String email;

@NotBlank(message = "can't be empty")
@Size(min = 3, max = 30, message = "length must be 3-30")
@DuplicatedUsernameConstraint
private String username;

@NotBlank(message = "can't be empty")
@Size(min = 8, max = 128, message = "length must be 8-128")
private String password;
}
95 changes: 45 additions & 50 deletions src/test/java/io/spring/api/ArticleApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest({ArticleApi.class})
@Import({WebSecurityConfig.class, JacksonCustomizations.class})
@WebMvcTest({ ArticleApi.class })
@Import({ WebSecurityConfig.class, JacksonCustomizations.class })
public class ArticleApiTest extends TestWithCurrentUser {
@Autowired private MockMvc mvc;
@Autowired
private MockMvc mvc;

@MockBean private ArticleQueryService articleQueryService;
@MockBean
private ArticleQueryService articleQueryService;

@MockBean private ArticleRepository articleRepository;
@MockBean
private ArticleRepository articleRepository;

@MockBean ArticleCommandService articleCommandService;
@MockBean
ArticleCommandService articleCommandService;

@Override
@BeforeEach
Expand All @@ -56,14 +60,13 @@ public void setUp() throws Exception {
public void should_read_article_success() throws Exception {
String slug = "test-new-article";
DateTime time = new DateTime();
Article article =
new Article(
"Test New Article",
"Desc",
"Body",
Arrays.asList("java", "spring", "jpg"),
user.getId(),
time);
Article article = new Article(
"Test New Article",
"Desc",
"Body",
Arrays.asList("java", "spring", "jpg"),
user.getId(),
time);
ArticleData articleData = TestHelper.getArticleDataFromArticleAndUser(article, user);

when(articleQueryService.findBySlug(eq(slug), eq(null))).thenReturn(Optional.of(articleData));
Expand All @@ -80,25 +83,21 @@ public void should_read_article_success() throws Exception {
@Test
public void should_404_if_article_not_found() throws Exception {
when(articleQueryService.findBySlug(anyString(), any())).thenReturn(Optional.empty());
RestAssuredMockMvc.when().get("/articles/not-exists").then().statusCode(404);
RestAssuredMockMvc.when().get("/articles/not-exists").then().statusCode(404).body("errors.body[0]", equalTo("article not found"));
}

@Test
public void should_update_article_content_success() throws Exception {
List<String> tagList = Arrays.asList("java", "spring", "jpg");

Article originalArticle =
new Article("old title", "old description", "old body", tagList, user.getId());
Article originalArticle = new Article("old title", "old description", "old body", tagList, user.getId());

Article updatedArticle =
new Article("new title", "new description", "new body", tagList, user.getId());
Article updatedArticle = new Article("new title", "new description", "new body", tagList, user.getId());

Map<String, Object> updateParam =
prepareUpdateParam(
updatedArticle.getTitle(), updatedArticle.getBody(), updatedArticle.getDescription());
Map<String, Object> updateParam = prepareUpdateParam(
updatedArticle.getTitle(), updatedArticle.getBody(), updatedArticle.getDescription());

ArticleData updatedArticleData =
TestHelper.getArticleDataFromArticleAndUser(updatedArticle, user);
ArticleData updatedArticleData = TestHelper.getArticleDataFromArticleAndUser(updatedArticle, user);

when(articleRepository.findBySlug(eq(originalArticle.getSlug())))
.thenReturn(Optional.of(originalArticle));
Expand Down Expand Up @@ -127,29 +126,27 @@ public void should_get_403_if_not_author_to_update_article() throws Exception {

User anotherUser = new User("test@test.com", "test", "123123", "", "");

Article article =
new Article(
title, description, body, Arrays.asList("java", "spring", "jpg"), anotherUser.getId());
Article article = new Article(
title, description, body, Arrays.asList("java", "spring", "jpg"), anotherUser.getId());

DateTime time = new DateTime();
ArticleData articleData =
new ArticleData(
article.getId(),
article.getSlug(),
article.getTitle(),
article.getDescription(),
article.getBody(),
false,
0,
time,
time,
Arrays.asList("joda"),
new ProfileData(
anotherUser.getId(),
anotherUser.getUsername(),
anotherUser.getBio(),
anotherUser.getImage(),
false));
ArticleData articleData = new ArticleData(
article.getId(),
article.getSlug(),
article.getTitle(),
article.getDescription(),
article.getBody(),
false,
0,
time,
time,
Arrays.asList("joda"),
new ProfileData(
anotherUser.getId(),
anotherUser.getUsername(),
anotherUser.getBio(),
anotherUser.getImage(),
false));

when(articleRepository.findBySlug(eq(article.getSlug()))).thenReturn(Optional.of(article));
when(articleQueryService.findBySlug(eq(article.getSlug()), eq(user)))
Expand All @@ -171,8 +168,7 @@ public void should_delete_article_success() throws Exception {
String body = "body";
String description = "description";

Article article =
new Article(title, description, body, Arrays.asList("java", "spring", "jpg"), user.getId());
Article article = new Article(title, description, body, Arrays.asList("java", "spring", "jpg"), user.getId());
when(articleRepository.findBySlug(eq(article.getSlug()))).thenReturn(Optional.of(article));

given()
Expand All @@ -193,9 +189,8 @@ public void should_403_if_not_author_delete_article() throws Exception {

User anotherUser = new User("test@test.com", "test", "123123", "", "");

Article article =
new Article(
title, description, body, Arrays.asList("java", "spring", "jpg"), anotherUser.getId());
Article article = new Article(
title, description, body, Arrays.asList("java", "spring", "jpg"), anotherUser.getId());

when(articleRepository.findBySlug(eq(article.getSlug()))).thenReturn(Optional.of(article));
given()
Expand Down