diff --git a/.gitignore b/.gitignore index 57f1cb2..9ef9c26 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,63 @@ -/.idea/ \ No newline at end of file +/.idea/ + +# Compiled class files +*.class + +# Log files +*.log + +# Package files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +build/ +.gradle/ +!gradle/wrapper/gradle-wrapper.jar + +# Eclipse +.classpath +.project +.settings/ +bin/ + +# IntelliJ +*.iml +*.iws +*.ipr +out/ + +# NetBeans +nbproject/private/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +# VS Code +.vscode/ + +# OS files +.DS_Store +Thumbs.db + +# Virtual machine crash logs +hs_err_pid* +replay_pid* diff --git a/.gitignore.txt b/.gitignore.txt deleted file mode 100644 index 57f1cb2..0000000 --- a/.gitignore.txt +++ /dev/null @@ -1 +0,0 @@ -/.idea/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 09f1083..0a6973e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.2.RELEASE") + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.18") } } diff --git a/pom.xml b/pom.xml index 63f5cbd..75edc18 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.springframework gs-spring-boot - pom + jar 0.1.0 org.springframework.boot spring-boot-starter-parent - 2.0.2.RELEASE + 2.7.18 @@ -28,10 +28,24 @@ org.springframework.boot spring-boot-starter-jdbc + + org.springframework.boot + spring-boot-starter-validation + com.h2database h2 + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/src/main/java/hello/Application.java b/src/main/java/hello/Application.java index 7cf8faf..53605fa 100644 --- a/src/main/java/hello/Application.java +++ b/src/main/java/hello/Application.java @@ -5,18 +5,14 @@ import java.util.stream.Collectors; import hello.model.Customer; -import hello.model.Quote; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.web.client.RestTemplate; @SpringBootApplication public class Application implements CommandLineRunner { @@ -34,25 +30,6 @@ public static void main(String[] args) { for (String beanName : beanNames) { System.out.println(beanName); } - - RestTemplate restTemplate = new RestTemplate(); - Quote quote = restTemplate.getForObject("http://gturnquist-quoters.cfapps.io/api/random", Quote.class); - log.info(quote.toString()); - } - - - @Bean - public RestTemplate restTemplate(RestTemplateBuilder builder) { - return builder.build(); - } - - @Bean - public CommandLineRunner run(RestTemplate restTemplate) throws Exception { - return args -> { - Quote quote = restTemplate.getForObject( - "http://gturnquist-quoters.cfapps.io/api/random", Quote.class); - log.info(quote.toString()); - }; } diff --git a/src/main/java/hello/controller/HelloController.java b/src/main/java/hello/controller/HelloController.java index f3602fa..a1cdfb8 100644 --- a/src/main/java/hello/controller/HelloController.java +++ b/src/main/java/hello/controller/HelloController.java @@ -1,23 +1,16 @@ package hello.controller; +import hello.declaration.SimpleTimeClient; import hello.declaration.TimeClient; -import hello.model.SimpleTimeClient; -import hello.model.Topic; import hello.service.TopicService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RequestMapping; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.ZoneId; -import java.time.chrono.ChronoPeriod; import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.regex.Pattern; -import java.util.stream.Collector; -import java.util.stream.Collectors; @RestController public class HelloController { @@ -43,7 +36,7 @@ public class HelloController { * * @return */ - @RequestMapping("/datetime") + @GetMapping("/datetime") public String index() { TimeClient myTimeClient = new SimpleTimeClient(); LocalDateTime localDateTime = LocalDateTime.now(); @@ -63,7 +56,7 @@ public String index() { * * @return */ - @RequestMapping("/topic/string/operation") + @GetMapping("/topic/string/operation") public String showStringOperation() { String join = topicService.returnAllTopicIDWithStringSlicing(); @@ -84,7 +77,7 @@ public String showStringOperation() { * File Operation in Java 8 * @return */ - @RequestMapping("/topic/file/operation") + @GetMapping("/topic/file/operation") public String showFileOperation() { String findAllFilesInPathAndSort = topicService.findAllFilesInPathAndSort(); String findParticularFileInPathAndSort = topicService.findParticularFileInPathAndSort(); diff --git a/src/main/java/hello/controller/TopicController.java b/src/main/java/hello/controller/TopicController.java index c2e8273..4c30d35 100644 --- a/src/main/java/hello/controller/TopicController.java +++ b/src/main/java/hello/controller/TopicController.java @@ -3,8 +3,17 @@ import hello.model.Topic; import hello.service.TopicService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; import java.util.List; @RestController @@ -18,9 +27,9 @@ public class TopicController { * Get all Topic * @return */ - @RequestMapping("/topic") - public List getAllTopics() { - return topicService.getAllTopics(); + @GetMapping("/topic") + public ResponseEntity> getAllTopics() { + return ResponseEntity.ok(topicService.getAllTopics()); } /** @@ -28,18 +37,19 @@ public List getAllTopics() { * @param id * @return */ - @RequestMapping("/topic/{id}") - public Topic getTopicWithID(@PathVariable String id) { - return topicService.getTopicWithId(id); + @GetMapping("/topic/{id}") + public ResponseEntity getTopicWithID(@PathVariable String id) { + return ResponseEntity.ok(topicService.getTopicWithId(id)); } /** * Add a new topic in list * @param topic */ - @RequestMapping(method = RequestMethod.POST, value = "/topic") - public void addTopic(@RequestBody Topic topic) { + @PostMapping("/topic") + public ResponseEntity addTopic(@Valid @RequestBody Topic topic) { topicService.addTopic(topic); + return ResponseEntity.status(HttpStatus.CREATED).build(); } /** @@ -47,9 +57,10 @@ public void addTopic(@RequestBody Topic topic) { * @param id * @param topic */ - @RequestMapping(method = RequestMethod.PUT, value = "/topic/{id}") - public void updateTopic(@PathVariable String id, @RequestBody Topic topic) { + @PutMapping("/topic/{id}") + public ResponseEntity updateTopic(@PathVariable String id, @Valid @RequestBody Topic topic) { topicService.updateTopic(id, topic); + return ResponseEntity.ok().build(); } @@ -57,9 +68,10 @@ public void updateTopic(@PathVariable String id, @RequestBody Topic topic) { * Delete a topic with ID * @param id */ - @RequestMapping(method = RequestMethod.DELETE, value = "/topic/{id}") - public void deleteTopic(@PathVariable String id) { + @DeleteMapping("/topic/{id}") + public ResponseEntity deleteTopic(@PathVariable String id) { topicService.deleteTopic(id); + return ResponseEntity.noContent().build(); } /** @@ -67,9 +79,9 @@ public void deleteTopic(@PathVariable String id) { * @param minLength * @return */ - @RequestMapping(value = "/topic/minimum/length/{minLength}") - public List filterMinimumLengthForId(@PathVariable Integer minLength) { - return topicService.filterMinimumLengthForId(minLength); + @GetMapping("/topic/minimum/length/{minLength}") + public ResponseEntity> filterMinimumLengthForId(@PathVariable Integer minLength) { + return ResponseEntity.ok(topicService.filterMinimumLengthForId(minLength)); } @@ -77,9 +89,9 @@ public List filterMinimumLengthForId(@PathVariable Integer minLength) { * Sort with Id * @return */ - @RequestMapping("/topic/sort") - public List sortTopicsWithID() { - return topicService.sortTopicsWithID(); + @GetMapping("/topic/sort") + public ResponseEntity> sortTopicsWithID() { + return ResponseEntity.ok(topicService.sortTopicsWithID()); } diff --git a/src/main/java/hello/declaration/CustomPredicate.java b/src/main/java/hello/declaration/CustomPredicate.java index 46f0c44..af828ac 100644 --- a/src/main/java/hello/declaration/CustomPredicate.java +++ b/src/main/java/hello/declaration/CustomPredicate.java @@ -4,6 +4,7 @@ * Use as a Functional Interface * @param */ +@FunctionalInterface public interface CustomPredicate { boolean test(T t); } diff --git a/src/main/java/hello/model/SimpleTimeClient.java b/src/main/java/hello/declaration/SimpleTimeClient.java similarity index 95% rename from src/main/java/hello/model/SimpleTimeClient.java rename to src/main/java/hello/declaration/SimpleTimeClient.java index e4e142a..e1eac43 100644 --- a/src/main/java/hello/model/SimpleTimeClient.java +++ b/src/main/java/hello/declaration/SimpleTimeClient.java @@ -1,6 +1,4 @@ -package hello.model; - -import hello.declaration.TimeClient; +package hello.declaration; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/hello/exception/GlobalExceptionHandler.java b/src/main/java/hello/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..55a11d9 --- /dev/null +++ b/src/main/java/hello/exception/GlobalExceptionHandler.java @@ -0,0 +1,38 @@ +package hello.exception; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(TopicNotFoundException.class) + public ResponseEntity> handleTopicNotFound(TopicNotFoundException ex) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception ex) { + log.error("Unhandled exception", ex); + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + private ResponseEntity> buildResponse(HttpStatus status, String message) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now().toString()); + body.put("status", status.value()); + body.put("error", status.getReasonPhrase()); + body.put("message", message); + return ResponseEntity.status(status).body(body); + } +} diff --git a/src/main/java/hello/exception/TopicNotFoundException.java b/src/main/java/hello/exception/TopicNotFoundException.java new file mode 100644 index 0000000..7ae064c --- /dev/null +++ b/src/main/java/hello/exception/TopicNotFoundException.java @@ -0,0 +1,12 @@ +package hello.exception; + +public class TopicNotFoundException extends RuntimeException { + + public TopicNotFoundException(String message) { + super(message); + } + + public TopicNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/hello/model/Quote.java b/src/main/java/hello/model/Quote.java deleted file mode 100644 index 2178a6d..0000000 --- a/src/main/java/hello/model/Quote.java +++ /dev/null @@ -1,34 +0,0 @@ -package hello.model; - -public class Quote { - private String type; - private Value value; - - @Override - public String toString() { - return "Quote{" + - "type='" + type + '\'' + - ", value=" + value + - '}'; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Value getValue() { - return value; - } - - public void setValue(Value value) { - this.value = value; - } - - public Quote() { - - } -} diff --git a/src/main/java/hello/model/Topic.java b/src/main/java/hello/model/Topic.java index 483a4a7..64e6907 100644 --- a/src/main/java/hello/model/Topic.java +++ b/src/main/java/hello/model/Topic.java @@ -1,9 +1,18 @@ package hello.model; +import java.util.Objects; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; public class Topic { + @NotBlank private String id; + + @NotBlank private String subjectName; + + @Size(max = 500) private String subjectDescription; public Topic() { @@ -40,4 +49,28 @@ public Topic(String id, String subjectName, String subjectDescription) { this.subjectDescription = subjectDescription; } + @Override + public String toString() { + return "Topic{" + + "id='" + id + '\'' + + ", subjectName='" + subjectName + '\'' + + ", subjectDescription='" + subjectDescription + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Topic topic = (Topic) o; + return Objects.equals(id, topic.id) + && Objects.equals(subjectName, topic.subjectName) + && Objects.equals(subjectDescription, topic.subjectDescription); + } + + @Override + public int hashCode() { + return Objects.hash(id, subjectName, subjectDescription); + } + } diff --git a/src/main/java/hello/model/Value.java b/src/main/java/hello/model/Value.java deleted file mode 100644 index 272be70..0000000 --- a/src/main/java/hello/model/Value.java +++ /dev/null @@ -1,33 +0,0 @@ -package hello.model; - -public class Value { - private Long id; - private String quote; - - public Value() { - } - - @Override - public String toString() { - return "Value{" + - "id=" + id + - ", quote='" + quote + '\'' + - '}'; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getQuote() { - return quote; - } - - public void setQuote(String quote) { - this.quote = quote; - } -} diff --git a/src/main/java/hello/service/TopicService.java b/src/main/java/hello/service/TopicService.java index feffb92..eabe950 100644 --- a/src/main/java/hello/service/TopicService.java +++ b/src/main/java/hello/service/TopicService.java @@ -1,6 +1,7 @@ package hello.service; import hello.declaration.CustomPredicate; +import hello.exception.TopicNotFoundException; import hello.model.Topic; import org.springframework.stereotype.Service; @@ -10,6 +11,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -19,7 +21,7 @@ public class TopicService { - private List topics = new ArrayList<>(Arrays.asList( + private List topics = new CopyOnWriteArrayList<>(Arrays.asList( new Topic("spring", "Spring Framework", "Spring Framework Description"), new Topic("java", "Core Java", "Java Description"), new Topic("javascript", "javascript Framework", "javascript Framework Description") @@ -36,7 +38,10 @@ public List getAllTopics() { * @return */ public Topic getTopicWithId(String id) { - return topics.stream().filter(topic -> topic.getId().equals(id)).findFirst().get(); + return topics.stream() + .filter(topic -> topic.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new TopicNotFoundException("Topic not found with id: " + id)); } public void addTopic(Topic topic) { @@ -96,8 +101,9 @@ private static List printTopicsWithPredicate(List topicList, Custo * @return */ public List sortTopicsWithID() { - topics.sort(Comparator.comparing(Topic::getId)); - return topics; + return topics.stream() + .sorted(Comparator.comparing(Topic::getId)) + .collect(Collectors.toList()); } @@ -166,7 +172,7 @@ public String findIdHavingCharacter() { * @return */ public String findAllFilesInPathAndSort() { - try (Stream stream = Files.list(Paths.get(""))) { + try (Stream stream = Files.list(Paths.get(System.getProperty("user.dir")))) { String joined = stream .map(String::valueOf) .filter(path -> !path.startsWith(".")) @@ -183,7 +189,7 @@ public String findAllFilesInPathAndSort() { * @return */ public String findParticularFileInPathAndSort() { - Path start = Paths.get(""); + Path start = Paths.get(System.getProperty("user.dir")); int maxDepth = 25; try (Stream stream = Files.find(start, maxDepth, (path, attr) -> String.valueOf(path).startsWith("grad"))) { @@ -203,7 +209,7 @@ public String findParticularFileInPathAndSort() { * @return */ public String findParticularFileInPathAndSortWithWalkFunction() { - Path start = Paths.get(""); + Path start = Paths.get(System.getProperty("user.dir")); int maxDepth = 5; try (Stream stream = Files.walk(start, maxDepth)) { String joined = stream @@ -223,7 +229,7 @@ public String findParticularFileInPathAndSortWithWalkFunction() { * @return */ public String readFileWithStreamFunction() { - Path path = Paths.get("temp.txt"); + Path path = Paths.get(System.getProperty("java.io.tmpdir"), "springboot-java8-temp.txt"); System.out.println(); try (BufferedReader reader = Files.newBufferedReader(path)) { String lines = reader diff --git a/application.properties b/src/main/resources/application.properties similarity index 100% rename from application.properties rename to src/main/resources/application.properties diff --git a/src/test/java/hello/controller/TopicControllerTest.java b/src/test/java/hello/controller/TopicControllerTest.java new file mode 100644 index 0000000..00cede2 --- /dev/null +++ b/src/test/java/hello/controller/TopicControllerTest.java @@ -0,0 +1,121 @@ +package hello.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import hello.exception.GlobalExceptionHandler; +import hello.exception.TopicNotFoundException; +import hello.model.Topic; +import hello.service.TopicService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(TopicController.class) +@Import(GlobalExceptionHandler.class) +class TopicControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TopicService topicService; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void getAllTopicsReturns200WithJsonArray() throws Exception { + given(topicService.getAllTopics()).willReturn(Arrays.asList( + new Topic("java", "Core Java", "Java desc"), + new Topic("spring", "Spring Framework", "Spring desc") + )); + + mockMvc.perform(get("/topic")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", org.hamcrest.Matchers.hasSize(2))) + .andExpect(jsonPath("$[0].id").value("java")); + } + + @Test + void getTopicByIdReturns200WhenFound() throws Exception { + given(topicService.getTopicWithId("java")) + .willReturn(new Topic("java", "Core Java", "Java desc")); + + mockMvc.perform(get("/topic/java")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value("java")) + .andExpect(jsonPath("$.subjectName").value("Core Java")); + } + + @Test + void getTopicByIdReturns404WhenMissing() throws Exception { + given(topicService.getTopicWithId("missing")) + .willThrow(new TopicNotFoundException("Topic not found with id: missing")); + + mockMvc.perform(get("/topic/missing")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message").value("Topic not found with id: missing")); + } + + @Test + void createTopicReturns201() throws Exception { + Topic topic = new Topic("python", "Python", "Python desc"); + + mockMvc.perform(post("/topic") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(topic))) + .andExpect(status().isCreated()); + + verify(topicService).addTopic(any(Topic.class)); + } + + @Test + void updateTopicReturns200() throws Exception { + Topic topic = new Topic("java", "Java 17", "Updated desc"); + + mockMvc.perform(put("/topic/java") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(topic))) + .andExpect(status().isOk()); + + verify(topicService).updateTopic(eq("java"), any(Topic.class)); + } + + @Test + void deleteTopicReturns204() throws Exception { + mockMvc.perform(delete("/topic/java")) + .andExpect(status().isNoContent()); + + verify(topicService).deleteTopic("java"); + } + + @Test + void genericExceptionReturns500() throws Exception { + willThrow(new RuntimeException("boom")).given(topicService).deleteTopic("boom"); + + mockMvc.perform(delete("/topic/boom")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("boom")); + } +} diff --git a/src/test/java/hello/declaration/SimpleTimeClientTest.java b/src/test/java/hello/declaration/SimpleTimeClientTest.java new file mode 100644 index 0000000..9e54737 --- /dev/null +++ b/src/test/java/hello/declaration/SimpleTimeClientTest.java @@ -0,0 +1,60 @@ +package hello.declaration; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SimpleTimeClientTest { + + @Test + void defaultConstructorInitializesLocalDateTime() { + SimpleTimeClient client = new SimpleTimeClient(); + LocalDateTime dateTime = client.getLocalDateTime(); + assertNotNull(dateTime); + } + + @Test + void setTimeUpdatesOnlyTimeComponent() { + SimpleTimeClient client = new SimpleTimeClient(); + client.setTime(10, 30, 45); + + LocalDateTime dateTime = client.getLocalDateTime(); + assertEquals(10, dateTime.getHour()); + assertEquals(30, dateTime.getMinute()); + assertEquals(45, dateTime.getSecond()); + } + + @Test + void toStringReturnsIsoFormattedDateTime() { + SimpleTimeClient client = new SimpleTimeClient(); + String result = client.toString(); + assertNotNull(result); + assertEquals(client.getLocalDateTime().toString(), result); + } + + @Test + void defaultMethodGetZonedDateTimeReturnsZonedDateTime() { + TimeClient client = new SimpleTimeClient(); + ZonedDateTime zoned = client.getZonedDateTime("America/New_York"); + + assertNotNull(zoned); + assertEquals(ZoneId.of("America/New_York"), zoned.getZone()); + } + + @Test + void staticGetZoneIdReturnsSystemDefaultForInvalidZone() { + ZoneId zoneId = TimeClient.getZoneId("Not/AValidZone"); + assertEquals(ZoneId.systemDefault(), zoneId); + } + + @Test + void staticGetZoneIdReturnsParsedZoneForValidZone() { + ZoneId zoneId = TimeClient.getZoneId("UTC"); + assertEquals(ZoneId.of("UTC"), zoneId); + } +} diff --git a/src/test/java/hello/service/TopicServiceTest.java b/src/test/java/hello/service/TopicServiceTest.java new file mode 100644 index 0000000..18ecbe6 --- /dev/null +++ b/src/test/java/hello/service/TopicServiceTest.java @@ -0,0 +1,112 @@ +package hello.service; + +import hello.exception.TopicNotFoundException; +import hello.model.Topic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class TopicServiceTest { + + private TopicService topicService; + + @BeforeEach + void setUp() { + topicService = new TopicService(); + } + + @Test + void getAllTopicsReturnsInitialThreeTopics() { + List topics = topicService.getAllTopics(); + assertNotNull(topics); + assertEquals(3, topics.size()); + } + + @Test + void getTopicWithValidIdReturnsMatchingTopic() { + Topic topic = topicService.getTopicWithId("java"); + assertNotNull(topic); + assertEquals("java", topic.getId()); + assertEquals("Core Java", topic.getSubjectName()); + } + + @Test + void getTopicWithInvalidIdThrowsTopicNotFoundException() { + TopicNotFoundException ex = assertThrows( + TopicNotFoundException.class, + () -> topicService.getTopicWithId("does-not-exist") + ); + assertTrue(ex.getMessage().contains("does-not-exist")); + } + + @Test + void addTopicAppendsToList() { + Topic newTopic = new Topic("python", "Python Language", "Python Description"); + topicService.addTopic(newTopic); + + assertEquals(4, topicService.getAllTopics().size()); + assertEquals(newTopic, topicService.getTopicWithId("python")); + } + + @Test + void updateTopicReplacesExistingTopic() { + Topic updated = new Topic("java", "Java 17", "Updated Java Description"); + topicService.updateTopic("java", updated); + + Topic result = topicService.getTopicWithId("java"); + assertEquals("Java 17", result.getSubjectName()); + assertEquals("Updated Java Description", result.getSubjectDescription()); + } + + @Test + void deleteTopicRemovesFromList() { + topicService.deleteTopic("spring"); + + assertEquals(2, topicService.getAllTopics().size()); + assertThrows(TopicNotFoundException.class, + () -> topicService.getTopicWithId("spring")); + } + + @Test + void sortTopicsWithIDReturnsSortedListWithoutMutatingOriginal() { + List originalOrder = topicService.getAllTopics(); + List originalIds = originalOrder.stream().map(Topic::getId).collect(java.util.stream.Collectors.toList()); + + List sorted = topicService.sortTopicsWithID(); + List sortedIds = sorted.stream().map(Topic::getId).collect(java.util.stream.Collectors.toList()); + + assertEquals(java.util.Arrays.asList("java", "javascript", "spring"), sortedIds); + assertNotSame(originalOrder, sorted); + assertEquals(originalIds, originalOrder.stream().map(Topic::getId).collect(java.util.stream.Collectors.toList()), + "Original list order should not be mutated"); + } + + @Test + void filterMinimumLengthForIdFiltersById() { + List filtered = topicService.filterMinimumLengthForId(5); + assertEquals(2, filtered.size()); + assertTrue(filtered.stream().allMatch(t -> t.getId().length() > 5)); + } + + @Test + void stringOperationMethodsRunWithoutError() { + assertDoesNotThrow(() -> { + String join = topicService.returnAllTopicIDWithStringSlicing(); + assertNotNull(join); + assertFalse(join.isEmpty()); + + assertNotNull(topicService.makeDistinctAndSortCharacters(join)); + assertNotNull(topicService.splitAllIdWithColonSelectIDWithJavaKeywordThenSortThenJoin(join)); + assertNotNull(topicService.findIdHavingCharacter()); + }); + } +} diff --git a/target/classes/hello/Application.class b/target/classes/hello/Application.class deleted file mode 100644 index 245a020..0000000 Binary files a/target/classes/hello/Application.class and /dev/null differ diff --git a/target/classes/hello/controller/GreetingController.class b/target/classes/hello/controller/GreetingController.class deleted file mode 100644 index 1149660..0000000 Binary files a/target/classes/hello/controller/GreetingController.class and /dev/null differ diff --git a/target/classes/hello/controller/HelloController.class b/target/classes/hello/controller/HelloController.class deleted file mode 100644 index 79b82ce..0000000 Binary files a/target/classes/hello/controller/HelloController.class and /dev/null differ diff --git a/target/classes/hello/controller/TopicController.class b/target/classes/hello/controller/TopicController.class deleted file mode 100644 index c32cad5..0000000 Binary files a/target/classes/hello/controller/TopicController.class and /dev/null differ diff --git a/target/classes/hello/declaration/CustomPredicate.class b/target/classes/hello/declaration/CustomPredicate.class deleted file mode 100644 index e35803f..0000000 Binary files a/target/classes/hello/declaration/CustomPredicate.class and /dev/null differ diff --git a/target/classes/hello/declaration/TimeClient.class b/target/classes/hello/declaration/TimeClient.class deleted file mode 100644 index a077c3b..0000000 Binary files a/target/classes/hello/declaration/TimeClient.class and /dev/null differ diff --git a/target/classes/hello/model/Customer.class b/target/classes/hello/model/Customer.class deleted file mode 100644 index 06e7307..0000000 Binary files a/target/classes/hello/model/Customer.class and /dev/null differ diff --git a/target/classes/hello/model/Greeting.class b/target/classes/hello/model/Greeting.class deleted file mode 100644 index f875277..0000000 Binary files a/target/classes/hello/model/Greeting.class and /dev/null differ diff --git a/target/classes/hello/model/Quote.class b/target/classes/hello/model/Quote.class deleted file mode 100644 index 3ec6c6c..0000000 Binary files a/target/classes/hello/model/Quote.class and /dev/null differ diff --git a/target/classes/hello/model/SimpleTimeClient.class b/target/classes/hello/model/SimpleTimeClient.class deleted file mode 100644 index f9bdddd..0000000 Binary files a/target/classes/hello/model/SimpleTimeClient.class and /dev/null differ diff --git a/target/classes/hello/model/Topic.class b/target/classes/hello/model/Topic.class deleted file mode 100644 index 3a53aa5..0000000 Binary files a/target/classes/hello/model/Topic.class and /dev/null differ diff --git a/target/classes/hello/model/Value.class b/target/classes/hello/model/Value.class deleted file mode 100644 index 862a68f..0000000 Binary files a/target/classes/hello/model/Value.class and /dev/null differ diff --git a/target/classes/hello/service/TopicService.class b/target/classes/hello/service/TopicService.class deleted file mode 100644 index b9f4992..0000000 Binary files a/target/classes/hello/service/TopicService.class and /dev/null differ diff --git a/target/classes/public/index.html b/target/classes/public/index.html deleted file mode 100644 index 566549b..0000000 --- a/target/classes/public/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - \ No newline at end of file