claimResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimResolver.apply(claims);
+ }
+
+ /**
+ * Parses the JWT token and returns all claims contained in its payload.
+ *
+ * The method verifies the token signature using the secret key.
+ *
+ * @param token JWT token string
+ * @return Claims object containing all claims from the token payload
+ * @throws JwtException if the token is invalid or the signature does not match
+ */
+ private Claims extractAllClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(decodeSecretKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java b/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java
new file mode 100644
index 0000000..2c69368
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/dto/UserDTO.java
@@ -0,0 +1,25 @@
+package org.pkwmtt.security.token.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.pkwmtt.examCalendar.entity.GeneralGroup;
+import org.pkwmtt.examCalendar.entity.User;
+import org.pkwmtt.examCalendar.enums.Role;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@Accessors(chain = true)
+public class UserDTO {
+ private String email;
+ private String group;
+ private Role role;
+
+ public UserDTO (User user) {
+ this.email = user.getEmail();
+ this.role = user.getRole();
+ this.group = Optional.ofNullable(user.getGeneralGroup()).map(GeneralGroup::getName).orElse(null);
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java b/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java
new file mode 100644
index 0000000..f5d6749
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/filter/JwtFilter.java
@@ -0,0 +1,81 @@
+package org.pkwmtt.security.token.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.pkwmtt.examCalendar.entity.User;
+import org.pkwmtt.examCalendar.repository.UserRepository;
+import org.pkwmtt.security.token.JwtService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+
+@Component
+public class JwtFilter extends OncePerRequestFilter {
+
+ @Autowired
+ JwtService jwtService;
+
+ @Autowired
+ UserRepository userRepository;
+
+ /**
+ * Filters incoming HTTP requests to validate JWT tokens.
+ *
+ *
This filter:
+ * - Extracts the JWT token from the Authorization header.
+ * - Validates the token using JwtService.
+ * - Loads the user from UserRepository.
+ * - Sets the Spring Security Authentication in the SecurityContext.
+ *
+ * @param request the HttpServletRequest
+ * @param response the HttpServletResponse
+ * @param filterChain the FilterChain
+ * @throws ServletException if a servlet error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ String authHeader = request.getHeader("Authorization");
+ String token = null;
+ String email = null;
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ token = authHeader.substring(7);
+ email = jwtService.getUserEmailFromToken(token);
+ }
+
+ if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ User user = userRepository.findByEmail(email).orElseThrow();
+
+ if (jwtService.validateToken(token, user)) {
+ List authorities = List.of(
+ new SimpleGrantedAuthority("ROLE_" + user.getRole())
+ );
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(
+ user.getEmail(),
+ null,
+ authorities
+ );
+
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java b/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java
new file mode 100644
index 0000000..21ee1bb
--- /dev/null
+++ b/src/main/java/org/pkwmtt/security/token/utils/JwtUtils.java
@@ -0,0 +1,20 @@
+package org.pkwmtt.security.token.utils;
+
+import lombok.Getter;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+@Getter
+@Component
+public class JwtUtils {
+ // Secret key used for signing JWTs. If the environment variable JWT_SECRET_KEY
+ // is not set, a default value "TEST_SECRET" is used. This allows the application
+ // to start without a real secret, e.g., for local development or tests.
+ private final String secret;
+ private final long expirationMs = 1000L * 60 * 60 * 24 * 30 * 6;
+
+ public JwtUtils(Environment environment) {
+ // Get the secret key from environment variables, or fallback to "TEST_SECRET"
+ this.secret = environment.getProperty("JWT_SECRET_KEY", "TEST_SECRET");
+ }
+}
diff --git a/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java b/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java
deleted file mode 100644
index 6af41a8..0000000
--- a/src/main/java/org/pkwmtt/status/DatabaseStatusChecker.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.pkwmtt.status;
-
-
-import lombok.Getter;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import javax.sql.DataSource;
-import java.sql.SQLException;
-
-@Slf4j
-@Service
-public class DatabaseStatusChecker {
- @Getter
- private static boolean enabled = false;
-
- @Autowired
- DatabaseStatusChecker (DataSource dataSource) {
- try {
- enabled = dataSource.getConnection().isValid(2);
- } catch (SQLException e) {
- log.error("Couldn't check database connection. Service will be unavailable");
- }
- }
-}
diff --git a/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java b/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java
deleted file mode 100644
index e7a4731..0000000
--- a/src/main/java/org/pkwmtt/status/SystemStatusCheckerService.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.pkwmtt.status;
-
-import jakarta.annotation.PostConstruct;
-import org.pkwmtt.mail.config.MailConfig;
-import org.pkwmtt.timetable.TimetableCacheService;
-import org.pkwmtt.timetable.TimetableService;
-import org.springframework.stereotype.Service;
-
-
-@Service
-public class SystemStatusCheckerService {
-
- private String mailingStatus;
- private String databaseStatus;
- private String cacheStatus;
- private String timetableStatus;
-
- SystemStatusCheckerService () {
- checkStatuses();
- }
-
- @PostConstruct
- private void checkStatuses () {
- mailingStatus = assignStatus(MailConfig.isEnabled());
- databaseStatus = assignStatus(DatabaseStatusChecker.isEnabled());
- timetableStatus = assignStatus(TimetableService.isEnabled());
- cacheStatus = assignStatus(TimetableCacheService.isCacheAvailable());
- }
-
- public String getStatus () {
- return String.format(
- """
- Server: ✅;
- Services:
- Mail: %s
- Database: %s,
- Timetable: %s,
- Cache: %s
- """, mailingStatus, databaseStatus, timetableStatus, cacheStatus
- );
- }
-
-
- private String assignStatus (boolean condition) {
- return condition ? "✅" : "❌";
- }
-}
diff --git a/src/main/java/org/pkwmtt/status/SystemStatusController.java b/src/main/java/org/pkwmtt/status/SystemStatusController.java
deleted file mode 100644
index dd055c1..0000000
--- a/src/main/java/org/pkwmtt/status/SystemStatusController.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.pkwmtt.status;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/pkwmtt/system/status")
-@RequiredArgsConstructor
-public class SystemStatusController {
- private final SystemStatusCheckerService service;
-
- @GetMapping
- public ResponseEntity getSystemStatus () {
- return ResponseEntity.ok(service.getStatus());
- }
-
-}
diff --git a/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java b/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java
index 6bd4861..c36307d 100644
--- a/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java
+++ b/src/main/java/org/pkwmtt/timetable/TimetableCacheService.java
@@ -3,7 +3,6 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.Getter;
import org.jsoup.Jsoup;
import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException;
import org.pkwmtt.exceptions.WebPageContentNotAvailableException;
@@ -18,17 +17,12 @@
import java.util.List;
import java.util.Map;
-import static java.util.Objects.isNull;
-
@Service
public class TimetableCacheService {
private final TimetableParserService parser;
private final ObjectMapper mapper;
private final Cache cache;
- @Getter
- private static boolean cacheAvailable = true;
-
@Value("${main.url:https://podzial.mech.pk.edu.pl/stacjonarne/html/}")
private String mainUrl;
@@ -36,23 +30,6 @@ public TimetableCacheService (TimetableParserService parser, ObjectMapper mapper
this.parser = parser;
this.mapper = mapper;
cache = cacheManager.getCache("timetables");
-
- if (isNull(cache)) {
- cacheAvailable = false;
- }
- }
-
- /**
- * @return connection status
- */
- public static boolean isConnectionAvailable () {
- try {
- fetchData("https://podzial.mech.pk.edu.pl/stacjonarne/html/");
- return true;
- } catch (Exception e) {
- System.out.println(e.getMessage());
- return false;
- }
}
/**
@@ -64,13 +41,13 @@ public static boolean isConnectionAvailable () {
*/
public TimetableDTO getGeneralGroupSchedule (String generalGroupName)
throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException {
- var generalGroupList = getGeneralGroupsMap();
+ var generalGroupMap = getGeneralGroupsMap();
- if (!generalGroupList.containsKey(generalGroupName)) {
+ if (!generalGroupMap.containsKey(generalGroupName)) {
throw new SpecifiedGeneralGroupDoesntExistsException(generalGroupName);
}
- String groupUrl = generalGroupList.get(generalGroupName);
+ String groupUrl = generalGroupMap.get(generalGroupName);
String url = mainUrl + groupUrl;
String cacheKey = "timetable_" + generalGroupName;
var html = fetchData(url);
@@ -96,10 +73,7 @@ public TimetableDTO getGeneralGroupSchedule (String generalGroupName)
public Map getGeneralGroupsMap () throws WebPageContentNotAvailableException {
var url = mainUrl + "lista.html";
var html = fetchData(url);
- String json = cache.get(
- "generalGroupMap",
- () -> mapper.writeValueAsString(parser.parseGeneralGroups(html))
- );
+ String json = cache.get("generalGroupMap", () -> mapper.writeValueAsString(parser.parseGeneralGroups(html)));
return getMappedValue(
json, "generalGroupList", cache, new TypeReference<>() {
@@ -115,10 +89,7 @@ public Map getGeneralGroupsMap () throws WebPageContentNotAvaila
*/
public List getListOfHours () throws WebPageContentNotAvailableException {
String url = mainUrl + "plany/o25.html";
- String json = cache.get(
- "hourList",
- () -> mapper.writeValueAsString(parser.parseHours(fetchData(url)))
- );
+ String json = cache.get("hourList", () -> mapper.writeValueAsString(parser.parseHours(fetchData(url))));
List result = getMappedValue(
json, "hourList", cache, new TypeReference<>() {
diff --git a/src/main/java/org/pkwmtt/timetable/TimetableController.java b/src/main/java/org/pkwmtt/timetable/TimetableController.java
index 7110810..f7490d6 100644
--- a/src/main/java/org/pkwmtt/timetable/TimetableController.java
+++ b/src/main/java/org/pkwmtt/timetable/TimetableController.java
@@ -14,12 +14,12 @@
import static java.util.Objects.isNull;
@RestController
-@RequestMapping("/pkmwtt/api/v1/timetables")
+@RequestMapping("${apiPrefix}/timetables")
@RequiredArgsConstructor
public class TimetableController {
private final TimetableService service;
private final TimetableCacheService cachedService;
-
+
/**
* Provide schedule of specified group and filters if all provided
*
@@ -29,20 +29,15 @@ public class TimetableController {
* @throws WebPageContentNotAvailableException .
*/
@GetMapping("/{generalGroupName}")
- public ResponseEntity getGeneralGroupSchedule (
- @PathVariable String generalGroupName,
- @RequestParam(required = false, name = "sub") List subgroups)
- throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException,
- SpecifiedSubGroupDoesntExistsException, JsonProcessingException {
-
+ public ResponseEntity getGeneralGroupSchedule (@PathVariable String generalGroupName, @RequestParam(required = false, name = "sub") List subgroups)
+ throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, SpecifiedSubGroupDoesntExistsException, JsonProcessingException {
+
if (isNull(subgroups) || subgroups.isEmpty()) {
return ResponseEntity.ok(cachedService.getGeneralGroupSchedule(generalGroupName));
}
- return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule(
- generalGroupName, subgroups
- ));
+ return ResponseEntity.ok(service.getFilteredGeneralGroupSchedule(generalGroupName, subgroups));
}
-
+
/**
* Provides list of schedule hours
*
@@ -50,22 +45,20 @@ public ResponseEntity getGeneralGroupSchedule (
* @throws WebPageContentNotAvailableException .
*/
@GetMapping("/hours")
- public ResponseEntity> getListOfHours ()
- throws WebPageContentNotAvailableException {
+ public ResponseEntity> getListOfHours () throws WebPageContentNotAvailableException {
return ResponseEntity.ok(cachedService.getListOfHours());
}
-
+
/**
* Provides list of general groups
*
* @return list of general groups
*/
@GetMapping("/groups/general")
- public ResponseEntity> getListOfGeneralGroups ()
- throws WebPageContentNotAvailableException {
+ public ResponseEntity> getListOfGeneralGroups () throws WebPageContentNotAvailableException {
return ResponseEntity.ok(service.getGeneralGroupList());
}
-
+
/**
* Provides list of available subgroups for specified general group
*
@@ -75,10 +68,13 @@ public ResponseEntity> getListOfGeneralGroups ()
*/
@GetMapping("/groups/{generalGroupName}")
public ResponseEntity> getListOfAvailableGroups (@PathVariable String generalGroupName)
- throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException,
- WebPageContentNotAvailableException {
+ throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, WebPageContentNotAvailableException {
return ResponseEntity.ok(service.getAvailableSubGroups(generalGroupName));
}
-
-
+
+ @GetMapping("/{generalGroupName}/list")
+ public ResponseEntity> getListOfSubjects (@PathVariable String generalGroupName) {
+ return ResponseEntity.ok(service.getListOfSubjects(generalGroupName));
+ }
+
}
diff --git a/src/main/java/org/pkwmtt/timetable/TimetableService.java b/src/main/java/org/pkwmtt/timetable/TimetableService.java
index 849cd66..55d0ce5 100644
--- a/src/main/java/org/pkwmtt/timetable/TimetableService.java
+++ b/src/main/java/org/pkwmtt/timetable/TimetableService.java
@@ -2,12 +2,12 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException;
import org.pkwmtt.exceptions.SpecifiedSubGroupDoesntExistsException;
import org.pkwmtt.exceptions.WebPageContentNotAvailableException;
import org.pkwmtt.timetable.dto.DayOfWeekDTO;
+import org.pkwmtt.timetable.dto.SubjectDTO;
import org.pkwmtt.timetable.dto.TimetableDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -24,9 +24,6 @@
public class TimetableService {
private final TimetableCacheService cachedService;
- @Getter
- private static final boolean enabled = TimetableCacheService.isConnectionAvailable();
-
@Autowired
TimetableService (TimetableCacheService cachedService) {
this.cachedService = cachedService;
@@ -40,8 +37,7 @@ public class TimetableService {
* @throws JsonProcessingException if timetable conversion to JSON fails
*/
public List getAvailableSubGroups (String generalGroupName)
- throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException,
- WebPageContentNotAvailableException {
+ throws JsonProcessingException, SpecifiedGeneralGroupDoesntExistsException, WebPageContentNotAvailableException {
generalGroupName = generalGroupName.toUpperCase();
TimetableDTO timetable = cachedService.getGeneralGroupSchedule(generalGroupName);
@@ -80,8 +76,7 @@ public List getAvailableSubGroups (String generalGroupName)
* @throws WebPageContentNotAvailableException if source data can't be retrieved
*/
public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, List sub)
- throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException,
- JsonProcessingException {
+ throws WebPageContentNotAvailableException, SpecifiedGeneralGroupDoesntExistsException, JsonProcessingException {
generalGroupName = generalGroupName.toUpperCase();
@@ -93,9 +88,7 @@ public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, Li
}
}
- List schedule = cachedService
- .getGeneralGroupSchedule(generalGroupName)
- .getData();
+ List schedule = cachedService.getGeneralGroupSchedule(generalGroupName).getData();
for (var day : schedule) {
@@ -111,12 +104,24 @@ public TimetableDTO getFilteredGeneralGroupSchedule (String generalGroupName, Li
* @return List of general group's names
*/
public List getGeneralGroupList () throws WebPageContentNotAvailableException {
- return cachedService
- .getGeneralGroupsMap()
- .keySet()
- .stream()
- .sorted()
- .collect(Collectors.toList());
+ return cachedService.getGeneralGroupsMap().keySet().stream().sorted().collect(Collectors.toList());
+ }
+
+ public List getListOfSubjects (String generalGroupName) {
+ var subjectSet = new HashSet();
+ var schedule = cachedService.getGeneralGroupSchedule(generalGroupName);
+
+ schedule.getData().forEach(day -> {
+ day.getEven().forEach(subject -> addToSet(subjectSet, subject));
+ day.getOdd().forEach(subject -> addToSet(subjectSet, subject));
+ });
+
+ return subjectSet.stream().toList();
+ }
+
+ private void addToSet (Set subjectSet, SubjectDTO subject) {
+ subject.deleteTypeAndUnnecessaryCharactersFromName();
+ subjectSet.add(subject.getName());
}
}
diff --git a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java
index 7b71ce3..141c80a 100644
--- a/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java
+++ b/src/main/java/org/pkwmtt/timetable/dto/SubjectDTO.java
@@ -2,7 +2,7 @@
import lombok.*;
import lombok.experimental.Accessors;
-import org.pkwmtt.enums.SubjectType;
+import org.pkwmtt.examCalendar.enums.SubjectType;
import java.util.regex.Pattern;
@@ -13,15 +13,13 @@ public class SubjectDTO {
private String classroom;
private int rowId;
private SubjectType type;
-
-
- public void deleteTypeAndUnnecessaryCharactersFromName() {
- if (name.contains(" "))
+
+
+ public void deleteTypeAndUnnecessaryCharactersFromName () {
+ if (name.contains(" ")) {
this.name = name.substring(0, name.indexOf(' '));
-
- name = name
- .replaceAll("_", " ")
- .replaceAll(Pattern.quote("("), "")
- .replaceAll(Pattern.quote(")"), "");
+ }
+
+ name = name.replaceAll("_", " ").replaceAll(Pattern.quote("("), "").replaceAll(Pattern.quote(")"), "");
}
}
diff --git a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java
index af32752..46605d3 100644
--- a/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java
+++ b/src/main/java/org/pkwmtt/timetable/parser/TimetableParserService.java
@@ -7,7 +7,7 @@
import org.jsoup.select.Elements;
import org.pkwmtt.timetable.dto.DayOfWeekDTO;
import org.pkwmtt.timetable.dto.SubjectDTO;
-import org.pkwmtt.enums.SubjectType;
+import org.pkwmtt.examCalendar.enums.SubjectType;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties
index cd609c7..bff5ace 100644
--- a/src/main/resources/application-prod.properties
+++ b/src/main/resources/application-prod.properties
@@ -1,17 +1,31 @@
-spring.datasource.url=jdbc:mysql://localhost:3306/pktt?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
-spring.datasource.username=pkttuser
-spring.datasource.password=pkttpassword
-
-server.port=8080
-server.address=0.0.0.0
-
-spring.jpa.show-sql=true
+### Properties for deployment
+#Import .env variables
+spring.config.import=optional:file:.env[.properties]
+#Database
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
+spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.hibernate.ddl-auto=none
spring.datasource.hikari.initialization-fail-timeout=0
-
+#Server properties
+server.port=8080
+server.address=0.0.0.0
+#Logging
logging.file.name=logs/app.log
logging.file.path=logs
-
+#Cache
spring.cache.type=caffeine
+#Test
+logging.level.WireMock.my-mock=off
+#Mail
+spring.mail.host=smtp.gmail.com
+spring.mail.port=587
+spring.mail.username=${EMAIL_USERNAME:}
+spring.mail.password=${EMAIL_PASSWORD:}
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
+#Path
+apiPrefix=/pkwmtt/api/v1
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a227ab6..876e8ad 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,29 +1,31 @@
+### Properties for deployment
#Import .env variables
spring.config.import=optional:file:.env[.properties]
-
-spring.datasource.url=jdbc:mysql://localhost:3306/pktt?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
-spring.datasource.username=pkttuser
-spring.datasource.password=pkttpassword
-
-server.port=8080
-server.address=0.0.0.0
-
+#Database
+spring.datasource.url=${SPRING_DATASOURCE_URL}
+spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
+spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.hibernate.ddl-auto=none
spring.datasource.hikari.initialization-fail-timeout=0
-
+#Server properties
+server.port=8080
+server.address=0.0.0.0
+#Logging
logging.file.name=logs/app.log
logging.file.path=logs
-
+#Cache
spring.cache.type=caffeine
-
+#Test
logging.level.WireMock.my-mock=off
-
+#Mail
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=${EMAIL_USERNAME:}
spring.mail.password=${EMAIL_PASSWORD:}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
+#Path
+apiPrefix=/pkwmtt/api/v1
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index dcbbeca..b860289 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -6,12 +6,12 @@
+ converterClass="org.pkwmtt.global.config.HighlightingCompositeLogConverter"/>
- true
+ false
%d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg%n
diff --git a/src/test/java/org/pkwmtt/TESTS.md b/src/test/java/org/pkwmtt/TESTS.md
deleted file mode 100644
index 388fc8e..0000000
--- a/src/test/java/org/pkwmtt/TESTS.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# MockController Test Suite
-
-This repository contains a unit test for the `MockController` in a Spring Boot application using `@WebMvcTest`. It focuses on verifying the `/api/v1/hello` endpoint and includes Spring Security bypass configuration for testing purposes.
-
-## 📄 Overview
-
-- **Test Type**: Unit test (controller layer only)
-- **Frameworks**: Spring Boot, JUnit 5, MockMvc
-- **Security**: Bypassed using `@WithMockUser` and custom `SecurityConfig`
-- **Target Endpoint**: `GET /api/v1/hello`
-
-## 🧪 How It Works
-
-The test class uses:
-- `@WebMvcTest` to load only the web layer
-- `MockMvc` to simulate HTTP requests
-- `@WithMockUser` to mock an authenticated user
-- `@Import(SecurityConfig.class)` to override security filters for testing
-
-## ✅ Example Test Case
-
-```java
-@WithMockUser
-@Test
-public void getHello() throws Exception {
- mockMvc.perform(get("/api/v1/hello"))
- .andExpect(status().isOk())
- .andExpect(content().string("Hello"));
-}
diff --git a/src/test/java/org/pkwmtt/cache/CacheConfigTest.java b/src/test/java/org/pkwmtt/cache/CacheConfigTest.java
index 6aa8251..53e9c30 100644
--- a/src/test/java/org/pkwmtt/cache/CacheConfigTest.java
+++ b/src/test/java/org/pkwmtt/cache/CacheConfigTest.java
@@ -5,7 +5,9 @@
import org.pkwmtt.ValuesForTest;
import org.pkwmtt.timetable.TimetableCacheService;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;
+import org.springframework.test.context.ActiveProfiles;
import test.TestConfig;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
@@ -14,71 +16,70 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
+@SpringBootTest
+@ActiveProfiles("test")
class CacheConfigTest extends TestConfig {
+
@Autowired
private TimetableCacheService service;
-
+
@Autowired
private CacheManager cacheManager;
-
+
@BeforeEach
- public void initWireMock() {
- EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html"))
- .willReturn(aResponse()
- .withStatus(200)
- .withHeader("Content-Type", "text/*")
- .withBody(ValuesForTest.timetableHTML)));
-
- EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html"))
- .willReturn(aResponse()
- .withStatus(200)
- .withHeader("Content-Type", "text/*")
- .withBody(ValuesForTest.listHTML)));
+ public void initWireMock () {
+ EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")).willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/*")
+ .withBody(ValuesForTest.timetableHTML)));
+
+ EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")).willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/*")
+ .withBody(ValuesForTest.listHTML)));
}
-
+
@Test
- void testCacheKeyPresent_Schedule() {
+ void testCacheKeyPresent_Schedule () {
//given
-
+
//when
service.getGeneralGroupSchedule("12K1");
var cache = cacheManager.getCache("timetables");
-
+
//then
assertAll(
- () -> {
- assertThat(cache).isNotNull();
- assertThat(cache.get("generalGroupMap", String.class))
- .isEqualTo("{\"11K2\":\"plany/o8.html\",\"12K1\":\"plany/o25.html\",\"11A1\":\"plany/o1.html\",\"12K3\":\"plany/o27.html\",\"12K2\":\"plany/o26.html\"}");
- },
- () -> {
- var wrapper = cache.get("timetable_12K1");
- assertThat(wrapper).isNotNull();
- assertThat(wrapper.get()).isInstanceOf(String.class);
- }
+ () -> {
+ assertThat(cache).isNotNull();
+ assertThat(cache.get("generalGroupMap", String.class)).isEqualTo(
+ "{\"11K2\":\"plany/o8.html\",\"12K1\":\"plany/o25.html\",\"11A1\":\"plany/o1.html\",\"12K3\":\"plany/o27.html\",\"12K2\":\"plany/o26.html\"}");
+ }, () -> {
+ var wrapper = cache.get("timetable_12K1");
+ assertThat(wrapper).isNotNull();
+ assertThat(wrapper.get()).isInstanceOf(String.class);
+ }
);
}
-
+
@Test
- void testCacheKeyPresent_HoursList(){
+ void testCacheKeyPresent_HoursList () {
//given
-
+
//when
service.getListOfHours();
var cache = cacheManager.getCache("timetables");
-
+
//then
assertAll(
- () -> {
- assertThat(cache).isNotNull();
- assertThat(cache.get("hourList", String.class))
- .isEqualTo("[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]");
- },
- () -> {
- var wrapper = cache.get("hourList");
- assertThat(wrapper).isNotNull();
- assertThat(wrapper.get()).isInstanceOf(String.class);
- }
+ () -> {
+ assertThat(cache).isNotNull();
+ assertThat(cache.get("hourList", String.class)).isEqualTo(
+ "[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]");
+ }, () -> {
+ var wrapper = cache.get("hourList");
+ assertThat(wrapper).isNotNull();
+ assertThat(wrapper.get()).isInstanceOf(String.class);
+ }
);
}
}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/cache/CacheInspector.java b/src/test/java/org/pkwmtt/cache/CacheInspector.java
index 0b38463..fdf4749 100644
--- a/src/test/java/org/pkwmtt/cache/CacheInspector.java
+++ b/src/test/java/org/pkwmtt/cache/CacheInspector.java
@@ -6,6 +6,7 @@
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.stereotype.Component;
+import org.springframework.test.context.ActiveProfiles;
import java.util.Map;
@@ -13,6 +14,7 @@
@Component
@RequiredArgsConstructor
+@ActiveProfiles("test")
@SuppressWarnings("unused")
public class CacheInspector {
diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java
new file mode 100644
index 0000000..44c1549
--- /dev/null
+++ b/src/test/java/org/pkwmtt/examCalendar/ExamControllerTest.java
@@ -0,0 +1,853 @@
+package org.pkwmtt.examCalendar;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.pkwmtt.examCalendar.dto.ExamDto;
+import org.pkwmtt.examCalendar.entity.Exam;
+import org.pkwmtt.examCalendar.entity.ExamType;
+import org.pkwmtt.examCalendar.entity.StudentGroup;
+import org.pkwmtt.examCalendar.repository.ExamRepository;
+import org.pkwmtt.examCalendar.repository.ExamTypeRepository;
+import org.pkwmtt.examCalendar.repository.GroupRepository;
+import org.pkwmtt.timetable.TimetableService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * integration tests of ExamCalendar
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
+@ActiveProfiles("database")
+class ExamControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ExamTypeRepository examTypeRepository;
+
+ @Autowired
+ private ExamRepository examRepository;
+
+ @Autowired
+ private ObjectMapper mapper;
+
+ @Autowired
+ private GroupRepository groupRepository;
+
+ @Mock
+ private TimetableService timetableService;
+
+ @BeforeEach
+ void setupBeforeEach() {
+ examRepository.deleteAll();
+ examTypeRepository.deleteAll();
+ groupRepository.deleteAll();
+ }
+
+ //
+
+ /**
+ * check if addExam endpoint create new exam with correct URI and correct data
+ */
+ @Test
+ @Transactional
+ void addExamWithCorrectData() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto examDtoRequest = createExampleExamDto("Project");
+ String json = mapper.writeValueAsString(examDtoRequest);
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+
+ MvcResult result = mockMvc.perform(MockMvcRequestBuilders
+ .post("/pkwmtt/api/v1/exams")
+ .contentType("application/json")
+ .content(json)
+ ).andDo(print())
+ .andExpect(status().isCreated())
+ .andExpect(header().string("Location", containsString("/pkwmtt/api/v1/exams/")))
+ .andReturn();
+
+ String location = result.getResponse().getHeader("Location");
+ @SuppressWarnings("DataFlowIssue")
+ int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1));
+
+ Exam examResponse = examRepository.findById(id).orElseThrow();
+
+ Set responseSubgroups = examResponse.getGroups().stream()
+ .map(StudentGroup::getName)
+ .collect(Collectors.toSet());
+ Set responseGeneralGroups = responseSubgroups.stream()
+ .filter(g -> g.matches("^\\d.*"))
+ .collect(Collectors.toSet());
+ responseSubgroups.removeAll(responseGeneralGroups);
+
+ assertEquals(responseGeneralGroups, Set.of("12K"));
+ assertEquals(responseSubgroups, examDtoRequest.getSubgroups());
+
+ assertEquals(examDtoRequest.getTitle(), examResponse.getTitle());
+ assertEquals(examDtoRequest.getDescription(), examResponse.getDescription());
+// compare dates with minutes level precision
+ assertEquals(
+ examDtoRequest.getDate().truncatedTo(ChronoUnit.MINUTES),
+ examResponse.getExamDate().truncatedTo(ChronoUnit.MINUTES)
+ );
+
+ assertEquals(examDtoRequest.getExamType(), examResponse.getExamType().getName());
+ }
+
+ @Test
+ void addExamWithBlankExamTitle() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("title : must not be blank", result);
+ }
+
+ @Test
+ void addExamWithBlankExamDescription() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+// when
+ MvcResult result = assertPostRequest(status().isCreated(), requestData);
+
+ String location = result.getResponse().getHeader("Location");
+ @SuppressWarnings("DataFlowIssue")
+ int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1));
+
+ Exam examResponse = examRepository.findById(id).orElseThrow();
+ assertNull(examResponse.getDescription());
+ }
+
+ @Test
+ void addExamWithBlankDate() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("date : must not be null", result);
+ }
+
+ @Test
+ void addExamWithBlankExamGroups() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("generalGroups : must not be empty", result);
+ }
+
+ @Test
+ void addExamWithBlankGeneralGroups() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+// null generalGroups
+ .subgroups(Set.of("L04"))
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+// then
+ assertResponseMessage("generalGroups : must not be empty", result);
+ }
+
+ @Test
+ @Transactional
+ void addExamWithBlankSubgroups() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+// null subgroups
+ .build();
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+
+// when
+ MvcResult result = assertPostRequest(status().isCreated(), requestData);
+// then
+ String location = result.getResponse().getHeader("Location");
+ @SuppressWarnings("DataFlowIssue")
+ int id = Integer.parseInt(location.substring(location.lastIndexOf("/") + 1));
+ Exam examResponse = examRepository.findById(id).orElseThrow();
+
+ assertEquals("12K2", examResponse.getGroups().iterator().next().getName());
+ }
+
+ @Test
+ void addExamWithMultipleGeneralGroupsAndSubgroups() throws Exception {
+ // given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K1", "12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+// then
+ assertResponseMessage("Invalid group identifier: ambiguous general groups for subgroups", result);
+ }
+
+ @Test
+ void addExamWithNullExamTypes() throws Exception {
+// given
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType(null) // brak typu egzaminu
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+// no examType
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("examType : must not be null", result);
+ }
+
+ @Test
+ void addExamWithNotFutureDate() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().minusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("date : Date must be in the future", result);
+ }
+
+ @Test
+ void addExamWithEmptyStringExamTitle() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("title : must not be blank", result);
+ }
+
+ @Test
+ void addExamWithTooLongExamTitle() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("a".repeat(256)) // 256 znaków
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("title : max size of field is 255", result);
+ }
+
+ @Test
+ void addExamWithTooLongDescription() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") // 256 znaków
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("Project")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("description : max size of field is 255", result);
+ }
+
+ @Test
+ void addExamWithNonExistingExamType() throws Exception {
+// given
+ createExampleExamType("Project");
+ ExamDto requestData = ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("NonExistingExamType")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+
+// when
+ MvcResult result = assertPostRequest(status().isBadRequest(), requestData);
+
+// then
+ assertResponseMessage("Invalid exam type NonExistingExamType", result);
+ }
+
+
+ //
+
+ //
+ @Test
+ @Transactional
+ void modifyExamWithCorrectData() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+ ExamDto examDto = createExampleExamDto(examType.getName());
+
+ when(timetableService.getGeneralGroupList()).thenReturn(List.of("12K1","12K2","12K3"));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04","L04","P04"));
+
+// when
+ assertPutRequest(status().isNoContent(), examDto, id);
+
+// then
+ Exam responseExam = examRepository.findById(id).orElseThrow();
+
+ Set responseSubgroups = responseExam.getGroups().stream()
+ .map(StudentGroup::getName)
+ .collect(Collectors.toSet());
+ Set responseGeneralGroups = responseSubgroups.stream()
+ .filter(g -> g.matches("^\\d.*"))
+ .collect(Collectors.toSet());
+ responseSubgroups.removeAll(responseGeneralGroups);
+
+ assertEquals("Math exam", responseExam.getTitle());
+ assertEquals("first exam", responseExam.getDescription());
+ assertEquals(
+ LocalDateTime.now().plusDays(1).truncatedTo(ChronoUnit.MINUTES),
+ responseExam.getExamDate().truncatedTo(ChronoUnit.MINUTES)
+ );
+ assertEquals(Set.of("12K"), responseGeneralGroups);
+ assertEquals(Set.of("L04"), responseSubgroups);
+ }
+
+ @Test
+ void modifyExamWithIncorrectExamId() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+ ExamDto examDto = createExampleExamDto(examType.getName());
+
+ int invalidId = Integer.MAX_VALUE - 10;
+ assertNotEquals(invalidId, id);
+// when
+ MvcResult result = assertPutRequest(status().isNotFound(), examDto, invalidId);
+
+// then
+ assertResponseMessage("No such element with id: " + (invalidId), result);
+
+ }
+//
+
+ //
+ @Test
+ void deleteExamWithCorrectArguments() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+
+// when
+ assertDeleteRequest(status().isNoContent(), id);
+
+// then
+ assertTrue(examRepository.findById(id).isEmpty());
+ }
+
+ @Test
+ void deleteNonExistingExam() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+ int invalidId = Integer.MAX_VALUE - 10;
+ assertNotEquals(invalidId, id);
+
+// when
+ MvcResult result = assertDeleteRequest(status().isNotFound(), invalidId);
+
+// then
+ assertTrue(examRepository.findById(id).isPresent());
+ assertResponseMessage("No such element with id: " + (invalidId), result);
+ }
+
+ //
+
+ //
+
+ @Test
+ void getExamByIdWithCorrectId() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+
+// when
+ MvcResult result = assertGetByIdRequest(status().isOk(), id);
+ JsonNode responseNode = mapper.readTree(result.getResponse().getContentAsString());
+
+// then
+ assertEquals(exam.getTitle(), responseNode.get("title").asText());
+ assertEquals(exam.getDescription(), responseNode.get("description").asText());
+ assertEquals(
+ exam.getExamDate().truncatedTo(ChronoUnit.MINUTES),
+ LocalDateTime.parse(responseNode.get("examDate").textValue()).truncatedTo(ChronoUnit.MINUTES)
+ );
+// assertEquals(exam.getGroups(), responseNode.get("examGroups").asText());
+ assertEquals(mapper.readTree(mapper.writeValueAsString(exam.getExamType())), responseNode.get("examType"));
+ }
+
+ @Test
+ void getNonExistingExamById() throws Exception {
+// given
+ ExamType examType = createExampleExamType("Exam");
+ Exam exam = createExampleExam(examType);
+ int id = examRepository.save(exam).getExamId();
+ int invalidId = Integer.MAX_VALUE - 10;
+ assertNotEquals(invalidId, id);
+
+// when
+ MvcResult result = assertGetByIdRequest(status().isNotFound(), invalidId);
+
+// then
+ assertResponseMessage("No such element with id: " + (invalidId), result);
+ }
+
+//
+
+ @Test
+ void getExamsWithGeneralGroups() throws Exception {
+// given
+ Exam exam1 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex1", Set.of("12K2")));
+ Exam exam2 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex2", Set.of("12K2", "12K1")));
+ Exam exam3 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex3", Set.of("12A2")));
+ Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04")));
+
+// when
+ MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("12K2"));
+
+// then
+ JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString());
+ assertEquals(2, responseArray.size());
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam1.getTitle())));
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam2.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam4.getTitle())));
+ }
+
+ @Test
+ void getExamsWithSubgroups() throws Exception {
+// given
+ Exam exam1 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex1", Set.of("12K2")));
+ Exam exam2 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex2", Set.of("12K2", "11K2")));
+ Exam exam3 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex3", Set.of("12A2")));
+ Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04")));
+ Exam exam5 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex5", Set.of("11K", "L04")));
+
+// when
+ MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("11K2"), Set.of("L04","P04", "K04"));
+
+// then
+ JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString());
+ assertEquals(2, responseArray.size());
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam2.getTitle())));
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam5.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam1.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam4.getTitle())));
+ }
+
+ @Test
+ void getExamsWithSubgroupsUsingWholeYearIdentifier() throws Exception {
+// given
+ Exam exam1 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex1", Set.of("12K2")));
+ Exam exam2 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex2", Set.of("12K2", "11K2")));
+ Exam exam3 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex3", Set.of("12A2")));
+ Exam exam4 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex4", Set.of("12K", "L04")));
+ Exam exam5 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex5", Set.of("11K", "L04")));
+ Exam exam6 = examRepository.save(createAndSaveExamWithTitleAndGroups("ex6", Set.of("12K", "L04", "P04")));
+
+// when
+ MvcResult result = assertGetByGroupsRequest(status().isOk(), Set.of("12K"), Set.of("L04", "K04"));
+
+// then
+ JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString());
+ assertEquals(2, responseArray.size());
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam4.getTitle())));
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("title").asText().equals(exam6.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam1.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam2.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam3.getTitle())));
+ assertTrue(responseArray.valueStream().noneMatch(e -> e.get("title").asText().equals(exam5.getTitle())));
+ }
+
+ @Test
+ void getExamsMultipleGeneralGroupsAndSubgroups() throws Exception {
+ // when
+ MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("11K2", "12A1"), Set.of("L04"));
+ // then
+ assertResponseMessage("Invalid group identifier: ambiguous superior group identifier for subgroups",result);
+ }
+
+ @Test
+ void getExamsWithSwappedGroupNames() throws Exception {
+ // when
+ MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("K04"), Set.of("11K2", "12A1"));
+ // then
+ assertResponseMessage("Specified general group [K04] doesn't exists",result);
+ }
+
+ @Test
+ void getExamsWithInvalidSubgroup() throws Exception {
+ // when
+ MvcResult result = assertGetByGroupsRequest(status().isBadRequest(), Set.of("12K1,", "12K2"), Set.of("11K2"));
+ // then
+ assertResponseMessage("Specified sub group [11K2] doesn't exists",result);
+ }
+
+ //
+
+ @Test
+ void getExamTypesWhenExamTypesExists() throws Exception {
+// given
+ ExamType exam = createExampleExamType("Exam");
+ ExamType project = createExampleExamType("Project");
+
+// when
+ MvcResult result = assertGetExamTypesRequest(status().isOk());
+ JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString());
+
+// then
+ assertEquals(2, responseArray.size());
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("name").asText().equals(exam.getName())));
+ assertTrue(responseArray.valueStream().anyMatch(e -> e.get("name").asText().equals(project.getName())));
+ }
+
+ @Test
+ void getExamTypesWhenExamTypesNotExists() throws Exception {
+// given
+// when
+ MvcResult result = mockMvc.perform(MockMvcRequestBuilders
+ .get("/pkwmtt/api/v1/exams/exam-types")
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(status().isOk())
+ .andReturn();
+ JsonNode responseArray = mapper.readTree(result.getResponse().getContentAsString());
+
+// then
+ assertEquals(0, responseArray.size());
+ }
+
+ //
+
+ //
+
+ /**
+ * this method create examType object and add it to repository
+ *
+ * @param name of new examType
+ * @return created examType object
+ */
+ private ExamType createExampleExamType(String name) {
+ ExamType examType = ExamType.builder().name(name).build();
+ examTypeRepository.save(examType);
+ return examType;
+ }
+
+ /**
+ * this method don't add created Exam to repository, because in that case id of created Exam would be unreachable
+ *
+ * @param type ExamType object which is required argument of Exam
+ * @return created Exam
+ */
+ private Exam createExampleExam(ExamType type) {
+ List savedGroups = groupRepository.saveAll(Stream.of("12K2", "L04")
+ .map(g -> StudentGroup.builder().name(g).build())
+ .collect(Collectors.toList()));
+ return Exam.builder()
+ .title("Exam")
+ .description("Exam description")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .groups(new HashSet<>(savedGroups))
+ .examType(type)
+ .build();
+ }
+
+ private Exam createAndSaveExamWithTitleAndGroups(String title, Set groups) {
+ ExamType examType = examTypeRepository.findByName("Project")
+ .orElseGet(() -> createExampleExamType("Project"));
+
+ Set groupsFromRepository = groupRepository.findAll().stream().map(StudentGroup::getName).collect(Collectors.toSet());
+ groupRepository.saveAll(groups.stream().filter(g -> !groupsFromRepository.contains(g))
+ .map(g -> StudentGroup.builder().name(g).build())
+ .collect(Collectors.toList()));
+
+ Set groupsToSave = groupRepository.findAll().stream().filter(g -> groups.contains(g.getName())).collect(Collectors.toSet());
+
+ return Exam.builder()
+ .title(title)
+ .description("Exam description")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .groups(groupsToSave)
+ .examType(examType)
+ .build();
+ }
+
+ /**
+ * @param examTypeName name of type of exam as String
+ * @return created ExamDto
+ */
+ private ExamDto createExampleExamDto(String examTypeName) {
+ return ExamDto.builder()
+ .title("Math exam")
+ .description("first exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType(examTypeName)
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+ }
+
+ /**
+ * compare error message form response with expected value
+ *
+ * @param expectedMessage full message that is expected in response
+ * @param result response generated by mockMvc.perform() or one of assert[httpMethod]Request()
+ */
+ private void assertResponseMessage(String expectedMessage, MvcResult result) throws Exception {
+ JsonNode jsonResponse = mapper.readTree(result.getResponse().getContentAsString());
+ assertTrue(jsonResponse.has("message"));
+ assertEquals(expectedMessage, jsonResponse.get("message").asText());
+ }
+
+ /**
+ * method send POST request to ExamController with content as JSON attached to body and then check if response
+ * code is the same as expected
+ *
+ * @param expectedStatus status().[http response] (example: status().isCreated() )
+ * @param content object that would be mapped to JSON by ObjectMapper and then attached to request
+ * it could be dto object or Map
+ * @return MvcResult object which could be used to capture response body
+ */
+ private MvcResult assertPostRequest(ResultMatcher expectedStatus, Object content) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .post("/pkwmtt/api/v1/exams")
+ .contentType("application/json")
+ .content(mapper.writeValueAsString(content))
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ /**
+ * method send PUT request to ExamController with content as JSON attached to body and examId as pathID.
+ * Then check if response code is the same as expected
+ *
+ * @param expectedStatus status().[http response] (example: status().isNoContent() )
+ * @param content object that would be mapped to JSON by ObjectMapper and then attached to request
+ * @param pathId id of resource that would be updated
+ * @return MvcResult object which could be used to capture response body
+ */
+ private MvcResult assertPutRequest(ResultMatcher expectedStatus, Object content, int pathId) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .put("/pkwmtt/api/v1/exams/{id}", pathId)
+ .contentType("application/json")
+ .content(mapper.writeValueAsString(content))
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ /**
+ * method send DELETE request to ExamController with examId as pathID.
+ * Then check if response code is the same as expected
+ *
+ * @param expectedStatus status().[http response] (example: status().isNoContent() )
+ * @param pathId id of resource that would be deleted
+ * @return MvcResult object which could be used to capture response body
+ */
+ private MvcResult assertDeleteRequest(ResultMatcher expectedStatus, int pathId) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .delete("/pkwmtt/api/v1/exams/{id}", pathId)
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ /**
+ * method send GET request to ExamController at /pkwmtt/api/v1/exams/{id} URI with examId as pathID.
+ * Then check if response code is the same as expected
+ *
+ * @param expectedStatus status().[http response] (example: status().isOk() )
+ * @param pathId id of resource that would be returned
+ * @return MvcResult object which could be used to capture response body
+ */
+ private MvcResult assertGetByIdRequest(ResultMatcher expectedStatus, int pathId) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .get("/pkwmtt/api/v1/exams/{id}", pathId)
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ private MvcResult assertGetByGroupsRequest(ResultMatcher expectedStatus, Set generalGroups) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .get("/pkwmtt/api/v1/exams/by-groups")
+ .param("generalGroups", generalGroups.toArray(new String[0]))
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ private MvcResult assertGetByGroupsRequest(ResultMatcher expectedStatus, Set generalGroups, Set subgroups) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .get("/pkwmtt/api/v1/exams/by-groups")
+ .param("generalGroups", generalGroups.toArray(new String[0]))
+ .param("subgroups", subgroups.toArray(new String[0]))
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+ /**
+ * method send GET request to ExamController at /pkwmtt/api/v1/exams/exam-types URI.
+ * Then check if response code is the same as expected
+ *
+ * @param expectedStatus expectedStatus status().[http response] (example: status().isOk() )
+ * @return MvcResult object which could be used to capture response body
+ */
+ private MvcResult assertGetExamTypesRequest(ResultMatcher expectedStatus) throws Exception {
+ return mockMvc.perform(MockMvcRequestBuilders
+ .get("/pkwmtt/api/v1/exams/exam-types")
+ .contentType("application/json")
+ ).andDo(print())
+ .andExpect(expectedStatus)
+ .andReturn();
+ }
+
+//
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java
new file mode 100644
index 0000000..fd4edf7
--- /dev/null
+++ b/src/test/java/org/pkwmtt/examCalendar/ExamServiceTest.java
@@ -0,0 +1,803 @@
+package org.pkwmtt.examCalendar;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.pkwmtt.examCalendar.dto.ExamDto;
+import org.pkwmtt.examCalendar.entity.Exam;
+import org.pkwmtt.examCalendar.entity.ExamType;
+import org.pkwmtt.examCalendar.entity.StudentGroup;
+import org.pkwmtt.examCalendar.repository.ExamRepository;
+import org.pkwmtt.examCalendar.repository.ExamTypeRepository;
+import org.pkwmtt.examCalendar.repository.GroupRepository;
+import org.pkwmtt.exceptions.*;
+import org.pkwmtt.timetable.TimetableService;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+
+@ExtendWith(MockitoExtension.class)
+class ExamServiceTest {
+
+ @Mock
+ private ExamRepository examRepository;
+
+ @Mock
+ private GroupRepository groupRepository;
+
+ @Mock
+ private ExamTypeRepository examTypeRepository;
+
+ @Mock
+ private TimetableService timetableService;
+
+ @InjectMocks
+ private ExamService examService;
+
+ //
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - blank
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void testBlankSubgroupAndMoreArgumentsThatRequiredReturnedByService() {
+// given
+ Set g12K2 = Set.of("12K2");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = ExamDto.builder()
+ .title("title")
+ .description("description")
+ .date(date)
+ .examType("exam")
+ .generalGroups(new HashSet<>(g12K2))
+ .build();
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(g12K2);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+// more groups than in set
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K1", "12K2", "12K3")));
+ when(groupRepository.findAllByNameIn(g12K2)).thenReturn(new HashSet<>(Set.of()));
+ when(groupRepository.saveAll(anyList())).thenReturn(studentGroups);
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(g12K2);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> groupCaptor = ArgumentCaptor.forClass(List.class);
+ verify(groupRepository, times(1)).saveAll(groupCaptor.capture());
+ assertEquals("12K2", groupCaptor.getValue().getFirst().getName());
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, g12K2);
+ }
+
+ /**
+ * test specification
+ * generalGroup - 3 item
+ * subgroup - 0 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void addExamForMultipleGeneralGroupsWithEmptySubgroups() {
+ // given
+ Set generalGroups = Set.of("12K1", "12K2", "12K3");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(generalGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups));
+
+ when(groupRepository.findAllByNameIn(generalGroups)).thenReturn(new HashSet<>(Set.of()));
+ when(groupRepository.saveAll(anyList())).thenReturn(studentGroups);
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(generalGroups);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> groupCaptor = ArgumentCaptor.forClass(List.class);
+ verify(groupRepository, times(1)).saveAll(groupCaptor.capture());
+ Set capturedGroups = groupCaptor.getValue().stream().map(StudentGroup::getName).collect(Collectors.toSet());
+ assertEquals(generalGroups, capturedGroups);
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, generalGroups);
+ }
+
+
+ /**
+ * test specification
+ * generalGroup - 3 item
+ * subgroup - 2 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void shouldThrowWhenThereAreMoreThan1GeneralGroupsAndSubgroupsIsPresent() {
+ // given
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ Set generalGroups = Set.of("12K1", "12K2", "12K3");
+ Set subgroups = Set.of("L04", "L05");
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups));
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+ assertEquals("Invalid group identifier: ambiguous general groups for subgroups", exception.getMessage());
+ }
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 1 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void addExamForSingleGeneralGroupAndSingleSubgroup() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("K04");
+ when(timetableService.getAvailableSubGroups(any(String.class))).thenReturn(new ArrayList<>(List.of("K03", "K04", "L04")));
+ testExamServiceForSubgroups(generalGroups, subgroups);
+ }
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 4 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void addExamForSingleGeneralGroupAndMultipleSubgroup() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("K04", "P04", "L04", "L03");
+ when(timetableService.getAvailableSubGroups(any(String.class))).thenReturn(new ArrayList<>(List.of("K03", "K04", "P04", "L04", "L03")));
+ testExamServiceForSubgroups(generalGroups, subgroups);
+ }
+
+
+ /**
+ * test specification
+ * generalGroup - 0 item
+ * subgroup - 1 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void addExamForEmptyGeneralGroup() {
+ // given
+ Set generalGroups = Set.of();
+ Set subgroups = Set.of("K04");
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+ assertEquals("Invalid group identifier: general group is missing", exception.getMessage());
+ }
+
+ //
+
+ //
+
+ /**
+ * test specification
+ * generalGroup - 2 item
+ * subgroup - 0 items
+ * timetable service - available
+ * provided groups - don't match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void shouldThrowWhenGeneralGroupsDontMatchService() {
+ // given
+ Set generalGroups = Set.of("12K1", "12K2");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of()));
+// when
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+// then
+ assertEquals("Invalid group identifiers: [12K1, 12K2]", exception.getMessage());
+ }
+
+ @Test
+ void shouldThrowWhenNotAllGeneralGroupsMatchService() {
+ // given
+ Set generalGroups = Set.of("12K1", "12K2");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K1")));
+// when
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+// then
+ assertEquals("Invalid group identifiers: [12K2]", exception.getMessage());
+ }
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 3 items
+ * timetable service - available
+ * provided groups - partially match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void shouldThrowWhenSubgroupsDontMatchService() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("K04", "P04", "L04");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K2")));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K05"));
+// when
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+// then
+ String message = exception.getMessage();
+ assertTrue(message.startsWith("Invalid group identifiers:"));
+ assertFalse(message.contains("12K2"));
+ assertTrue(message.contains("K04"));
+ assertTrue(message.contains("P04"));
+ assertTrue(message.contains("L04"));
+ assertFalse(message.contains("K05"));
+ }
+
+ @Test
+ void shouldThrowWhenNotAllSubgroupsMatchService() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("K04", "P04", "L04");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(List.of("12K2")));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("P04", "L04", "K05"));
+// when
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.addExam(examDto));
+// then
+ String message = exception.getMessage();
+ assertTrue(message.startsWith("Invalid group identifiers:"));
+ assertFalse(message.contains("12K2"));
+ assertTrue(message.contains("K04"));
+ assertFalse(message.contains("P04"));
+ assertFalse(message.contains("L04"));
+ assertFalse(message.contains("K05"));
+ }
+
+ //
+
+ //
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 0 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - contain provided groups
+ */
+ @Test
+ void addExamForSingleGeneralGroupWithRepositoryContainingGroup() {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(generalGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups));
+
+ when(groupRepository.findAllByNameIn(generalGroups)).thenReturn(new HashSet<>(studentGroups));
+ when(groupRepository.saveAll(any())).thenReturn(List.of());
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(any()); //???
+ verify(groupRepository, times(1)).saveAll(List.of());
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, generalGroups);
+ }
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 4 items
+ * timetable service - available
+ * provided groups - match groups from timetable service
+ * groupRepository - partially contain provided groups
+ */
+ @Test
+ void addExamForSingleGeneralGroupAndSubgroupsWithRepositoryContainingGroups() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("K04", "P04", "L04", "K05");
+ Set combinedGroups = Set.of("12K", "K04", "P04", "L04", "K05");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(combinedGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups));
+ when(timetableService.getAvailableSubGroups("12K2")).thenReturn(List.of("K04", "P04", "L04", "K05"));
+
+ //noinspection unchecked
+ when(groupRepository.findAllByNameIn(any(Set.class))).thenReturn(new HashSet<>(studentGroups.subList(0, 3)));
+ when(groupRepository.saveAll(any())).thenReturn(studentGroups.subList(3, 5));
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(any());
+ verify(groupRepository, times(1)).saveAll(any());
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, combinedGroups);
+ }
+
+ //
+
+ //
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 0 item
+ * timetable service - unavailable
+ * provided groups - match groups from timetable service
+ * groupRepository - don't contain provided groups
+ */
+ @Test
+ void unavailableServiceAndRepositoryDontMatch() {
+// given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+
+// more groups than in set
+ when(timetableService.getGeneralGroupList()).thenThrow(new WebPageContentNotAvailableException());
+ when(groupRepository.findAllByNameIn(generalGroups)).thenReturn(new HashSet<>(Set.of()));
+// when
+ RuntimeException exception = assertThrows(ServiceNotAvailableException.class, () -> examService.addExam(examDto));
+// then
+ assertEquals("Couldn't verify groups using repository", exception.getMessage());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(generalGroups);
+ }
+
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 3 items
+ * timetable service - unavailable
+ * provided groups - match groups from timetable service
+ * groupRepository - partially contain provided groups
+ */
+ @Test
+ void unavailableServiceAndRepositoryDontMatchForSubgroups() throws JsonProcessingException {
+// given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("L04", "K04", "P04");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ List studentGroups = buildExampleStudentGroupList(Set.of("12K2", "L04"));
+
+// more groups than in set
+ when(timetableService.getGeneralGroupList()).thenThrow(new WebPageContentNotAvailableException());
+ when(timetableService.getAvailableSubGroups("12K2")).thenThrow(new WebPageContentNotAvailableException());
+ when(groupRepository.findAllByNameIn(any())).thenReturn(new HashSet<>(studentGroups));
+// when
+ RuntimeException exception = assertThrows(ServiceNotAvailableException.class, () -> examService.addExam(examDto));
+// then
+ assertEquals("Couldn't verify groups using timetable service", exception.getMessage());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(generalGroups);
+ }
+
+ //
+
+ //
+
+ /**
+ * test specification
+ * generalGroup - 2 item
+ * subgroup - 0 item
+ * timetable service - unavailable
+ * provided groups - match groups from timetable service
+ * groupRepository - contain provided groups
+ */
+ @Test
+ void addExamWhenServiceIsUnavailableAndRepositoryContainsGeneralGroups() {
+ // given
+ Set generalGroups = Set.of("12K1", "12K2");
+ Set subgroups = Set.of();
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(generalGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenThrow(new WebPageContentNotAvailableException());
+
+ when(groupRepository.findAllByNameIn(generalGroups)).thenReturn(new HashSet<>(studentGroups));
+ when(groupRepository.saveAll(anyList())).thenReturn(studentGroups);
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(2)).findAllByNameIn(any());
+ verify(groupRepository, times(1)).saveAll(any());
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, generalGroups);
+ }
+
+ /**
+ * test specification
+ * generalGroup - 1 item
+ * subgroup - 4 items
+ * timetable service - unavailable
+ * provided groups - match groups from timetable service
+ * groupRepository - contain provided groups
+ */
+ @Test
+ @Disabled("Not supported yet")
+ void addExamWhenServiceIsUnavailableAndRepositoryContainsGroups() throws JsonProcessingException {
+ // given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("L04", "K04", "P04", "K05");
+ Set combinedGroups = Set.of("12K2", "L04", "K04", "P04", "K05");
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(combinedGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenThrow(new WebPageContentNotAvailableException());
+ when(timetableService.getAvailableSubGroups("12K2")).thenThrow(new JsonParseException("parsing subgroups failed"));
+
+ //noinspection unchecked
+ when(groupRepository.findAllByNameIn(any(Set.class))).thenReturn(new HashSet<>(studentGroups));
+ when(groupRepository.saveAll(anyList())).thenReturn(List.of());
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(2)).findAllByNameIn(any());
+ verify(groupRepository, times(1)).saveAll(any());
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, generalGroups);
+ }
+
+ //
+
+ /************************************************************************************/
+//modify exam
+ @Test
+ void shouldModifyExamWhenIdExists() {
+
+ }
+
+ @Test
+ void shouldThrowWhenExamIdNotExists() {
+ // given
+
+ }
+
+ /************************************************************************************/
+//delete exam
+ @Test
+ void shouldDeleteExamWhenIdExists() {
+// given
+ int examId = 1;
+ when(examRepository.findById(examId)).thenReturn(Optional.of(mock(Exam.class)));
+// when
+ examService.deleteExam(examId);
+// then
+ verify(examRepository).deleteById(examId);
+ }
+
+ @Test
+ void shouldThrowExceptionWhenExamIdNotExists() {
+// given
+ int examId = 5;
+ when(examRepository.findById(examId)).thenThrow(new NoSuchElementException("Exam not found"));
+// when
+ RuntimeException exception = assertThrows(
+ NoSuchElementException.class,
+ () -> examService.deleteExam(examId)
+ );
+// then
+ verify(examRepository, never()).deleteById(examId);
+ assertEquals("Exam not found", exception.getMessage());
+ }
+
+ /************************************************************************************/
+// getExamById
+ @Test
+ void getExamById() {
+// given
+ int examId = 1;
+ when(examRepository.findById(examId)).thenReturn(Optional.of(mock(Exam.class)));
+// when
+ Exam exam = examService.getExamById(examId);
+// then
+ verify(examRepository).findById(examId);
+ assertNotNull(exam);
+ }
+
+ @Test
+ void shouldThrowExceptionWhenExamNotFound() {
+// given
+ int examId = 5;
+ when(examRepository.findById(examId)).thenThrow(new NoSuchElementException("Exam not found"));
+// when
+ RuntimeException exception = assertThrows(
+ NoSuchElementException.class,
+ () -> examService.getExamById(examId)
+ );
+// then
+ assertEquals("Exam not found", exception.getMessage());
+ }
+
+ // getExamByGroup
+
+ @Test
+ void getExamsForNormalGroups() {
+// given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of("L04", "K04", "P04");
+// when
+ examService.getExamByGroups(generalGroups, subgroups);
+// then
+ verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups);
+ verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("12K", subgroups);
+ }
+
+ @Test
+ void getExamsForGroupWithoutDigitAsFirstCharacter() {
+// given
+ Set generalGroups = Set.of("1Er");
+ Set subgroups = Set.of("L01", "K01", "P01");
+// when
+ examService.getExamByGroups(generalGroups, subgroups);
+// then
+ verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups);
+ verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("1Er", subgroups);
+ }
+
+ @Test
+ void getExamsWithEmptySubgroups() {
+// given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = Set.of();
+// when
+ examService.getExamByGroups(generalGroups, subgroups);
+// then
+ verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups);
+ verify(examRepository, never()).findAllBySubgroupsOfGeneralGroup(any(), any());
+ }
+
+ @Test
+ void getExamsWithBlankSubgroups() {
+// given
+ Set generalGroups = Set.of("12K2");
+ Set subgroups = null;
+// when
+ examService.getExamByGroups(generalGroups, subgroups);
+// then
+ verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups);
+ verify(examRepository, never()).findAllBySubgroupsOfGeneralGroup(any(), any());
+ }
+
+ @Test
+ void shouldNotThrowWhenGroupsAreFromTheSameYearOfStudy() {
+// given
+ Set generalGroups = Set.of("12K1", "12K2");
+ Set subgroups = Set.of("L01", "K01", "P01");
+// when
+ examService.getExamByGroups(generalGroups, subgroups);
+// then
+ verify(examRepository, times(1)).findAllByGroups_NameIn(generalGroups);
+ verify(examRepository, times(1)).findAllBySubgroupsOfGeneralGroup("12K", subgroups);
+ }
+
+ @Test
+ void shouldThrowWhenSubgroupsAreSwappedWithGeneralGroups() {
+// given
+ Set generalGroups = new HashSet<>(Set.of("L01", "K01", "P01"));
+ Set subgroups = new HashSet<>( Set.of("12K1"));
+// when then
+ assertThrows(
+ SpecifiedGeneralGroupDoesntExistsException.class,
+ () -> examService.getExamByGroups(generalGroups, subgroups)
+ );
+ }
+
+ @Test
+ void shouldThrowWhenSubgroupsAreTheGeneralGroups() {
+// given
+ Set generalGroups = new HashSet<>(Set.of("12K1"));
+ Set subgroups = new HashSet<>( Set.of("12K1", "12K2", "12K3"));
+// when, then
+ assertThrows(
+ SpecifiedSubGroupDoesntExistsException.class,
+ () -> examService.getExamByGroups(generalGroups, subgroups)
+ );
+ }
+
+ @Test
+ void shouldThrowWhenGeneralGroupsAreFromDifferentYearOfStudy() {
+// given
+ Set generalGroups = Set.of("12K1", "12A2");
+ Set subgroups = Set.of("L01", "K01", "P01");
+// when
+ RuntimeException exception = assertThrows(InvalidGroupIdentifierException.class, () -> examService.getExamByGroups(generalGroups, subgroups));
+// then
+ assertEquals("Invalid group identifier: ambiguous superior group identifier for subgroups", exception.getMessage());
+ }
+
+
+
+ private static List buildExampleStudentGroupList(Set groupNames) {
+ AtomicInteger id = new AtomicInteger();
+ return groupNames.stream()
+ .map(g -> StudentGroup.builder()
+ .groupId(id.getAndIncrement())
+ .name(g)
+ .build()
+ ).collect(Collectors.toList());
+ }
+
+ private static Exam buildExamWithIdAndGroups(int id, List groups) {
+ return Exam.builder()
+ .examId(id)
+ .groups(new HashSet<>(groups))
+ .build();
+ }
+
+ private static ExamType buildExampleExamType() {
+ return ExamType.builder()
+ .examTypeId(1)
+ .name("exam")
+ .build();
+ }
+
+ private static ExamDto buildExampleExamDto(Set generalGroups, Set subgroups, LocalDateTime date) {
+ return ExamDto.builder()
+ .title("title")
+ .description("description")
+ .date(date)
+ .examType("exam")
+ .generalGroups(new HashSet<>(generalGroups))
+ .subgroups(new HashSet<>(subgroups))
+ .build();
+ }
+
+ private static void assertExam(Exam savedExam, LocalDateTime date, int savedId, Set groups) {
+ assertEquals("title", savedExam.getTitle());
+ assertEquals("description", savedExam.getDescription());
+ assertEquals(date, savedExam.getExamDate());
+ assertEquals("exam", savedExam.getExamType().getName());
+ assertEquals(groups.size(), savedExam.getGroups().size());
+ assertEquals(groups, savedExam.getGroups().stream().map(StudentGroup::getName).collect(Collectors.toSet()));
+ assertEquals(1, savedId);
+ }
+
+ private void testExamServiceForSubgroups(Set generalGroups, Set subgroups) {
+ Set combinedGroups = new HashSet<>(subgroups);
+ combinedGroups.addAll(generalGroups.stream()
+ .map(g -> g.matches(".*\\d$") ? g.substring(0, g.length() - 1) : g)
+ .collect(Collectors.toSet()));
+
+ LocalDateTime date = LocalDateTime.now().plusDays(1);
+ ExamDto examDto = buildExampleExamDto(generalGroups, subgroups, date);
+ ExamType examType = buildExampleExamType();
+ List studentGroups = buildExampleStudentGroupList(combinedGroups);
+ Exam exam = buildExamWithIdAndGroups(1, studentGroups);
+
+ when(examTypeRepository.findByName(examDto.getExamType())).thenReturn(Optional.of(examType));
+ when(timetableService.getGeneralGroupList()).thenReturn(new ArrayList<>(generalGroups));
+
+ when(groupRepository.findAllByNameIn(combinedGroups)).thenReturn(new HashSet<>(Set.of()));
+ when(groupRepository.saveAll(anyList())).thenReturn(studentGroups);
+ when(examRepository.save(any(Exam.class))).thenReturn(exam);
+// when
+ int savedId = examService.addExam(examDto);
+// then
+ verify(examTypeRepository, times(1)).findByName(examDto.getExamType());
+ verify(timetableService, times(1)).getGeneralGroupList();
+ verify(groupRepository, times(1)).findAllByNameIn(combinedGroups);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> groupCaptor = ArgumentCaptor.forClass(List.class);
+ verify(groupRepository, times(1)).saveAll(groupCaptor.capture());
+ Set capturedGroups = groupCaptor.getValue().stream().map(StudentGroup::getName).collect(Collectors.toSet());
+ assertEquals(combinedGroups, capturedGroups);
+
+ ArgumentCaptor examCaptor = ArgumentCaptor.forClass(Exam.class);
+ verify(examRepository, times(1)).save(examCaptor.capture());
+ Exam savedExam = examCaptor.getValue();
+ assertExam(savedExam, date, savedId, combinedGroups);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java
new file mode 100644
index 0000000..a98fd0e
--- /dev/null
+++ b/src/test/java/org/pkwmtt/examCalendar/dto/ExamDtoTest.java
@@ -0,0 +1,230 @@
+package org.pkwmtt.examCalendar.dto;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.pkwmtt.examCalendar.entity.StudentGroup;
+
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ExamDtoTest {
+
+ private final Validator validator;
+
+ public ExamDtoTest() {
+ this.validator = Validation.buildDefaultValidatorFactory().getValidator();
+ }
+
+ @Test
+ void shouldSuccessWithCompleteData() {
+// given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when, then
+ assertTrue(validator.validate(examDto).isEmpty());
+ }
+
+ @Test
+ void shouldSuccessWithEmptyDescription() {
+// given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when, then
+ assertTrue(validator.validate(examDto).isEmpty());
+ }
+
+ @Test
+ void shouldSuccessWithBlankDescription() {
+// given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when, then
+ assertTrue(validator.validate(examDto).isEmpty());
+ }
+
+ @Test
+ void shouldSuccessWithBlankSubgroups() {
+// given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .build();
+// when, then
+ assertTrue(validator.validate(examDto).isEmpty());
+ }
+
+ @Test
+ void shouldSuccessWithEmptySubgroups() {
+// given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of(""))
+ .build();
+// when, then
+ assertTrue(validator.validate(examDto).isEmpty());
+ }
+
+
+ // empty Strings
+ @Test
+ void shouldFailWithEmptyTitle() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+ .title("")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of(""))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("title")));
+ }
+
+ @Test
+ void shouldFailWithBlankTitle() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of(""))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("title")));
+ }
+
+ @Test
+ void shouldFailWithEmptyGeneralGroups() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of())
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("generalGroups")));
+ }
+
+ @Test
+ void shouldFailWithBlankGeneralGroups() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+ .title("Math exam")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("generalGroups")));
+ }
+
+// to long Strings
+
+ @Test
+ void ShouldFailWithTooLongTitle() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+// 256 characters
+ .title("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ .description("First exam")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("title")));
+ }
+
+ @Test
+ void toLongDescription() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+// 256 characters
+ .title("Math exam")
+ .description("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ .date(LocalDateTime.now().plusDays(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+// when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("description")));
+ }
+
+ @Test
+ void dateNotInFuture() {
+ // given
+ ExamDto examDto = ExamDto.builder()
+// 256 characters
+ .title("Math exam")
+ .description("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ .date(LocalDateTime.now().minusHours(1))
+ .examType("exam")
+ .generalGroups(Set.of("12K2"))
+ .subgroups(Set.of("L04"))
+ .build();
+ // when
+ Set> violations = validator.validate(examDto);
+// then
+ assertFalse(validator.validate(examDto).isEmpty());
+ assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("date")));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/examCalendar/entity/ExamTest.java b/src/test/java/org/pkwmtt/examCalendar/entity/ExamTest.java
new file mode 100644
index 0000000..d813ede
--- /dev/null
+++ b/src/test/java/org/pkwmtt/examCalendar/entity/ExamTest.java
@@ -0,0 +1,87 @@
+package org.pkwmtt.examCalendar.entity;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.pkwmtt.exceptions.UnsupportedCountOfArgumentsException;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * tests of custom Exam builder
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class ExamTest {
+
+ ExamType examType;
+ Set studentGroups;
+ LocalDateTime date;
+
+ @BeforeAll
+ void setup(){
+ examType = ExamType.builder().name("project").build();
+ studentGroups = Set.of(StudentGroup.builder().name("12K2").build());
+ date = LocalDateTime.now().plusDays(1);
+
+ }
+
+ @Test
+ void shouldBuildExamWithCorrectData() {
+ Exam exam = Exam.builder()
+ .title("title")
+ .description("description")
+ .examDate(date)
+ .examType(examType)
+ .groups(studentGroups)
+ .build();
+
+ assertEquals("title", exam.getTitle());
+ assertEquals("description", exam.getDescription());
+ assertEquals(date, exam.getExamDate());
+ assertEquals(examType, exam.getExamType());
+ assertEquals(studentGroups, exam.getGroups());
+ }
+
+ @Test
+ void shouldThrowWhenNoGroupsAssigned() {
+ assertThrows(UnsupportedCountOfArgumentsException.class, () -> Exam.builder()
+ .title("title")
+ .description("description")
+ .examDate(date)
+ .examType(examType)
+// no exam groups specified
+ .build());
+ }
+
+ @Test
+ void shouldThrowWhenZeroGroupsAssigned() {
+ assertThrows(UnsupportedCountOfArgumentsException.class, () -> Exam.builder()
+ .title("title")
+ .description("description")
+ .examDate(date)
+ .examType(examType)
+ .groups(Set.of())
+ .build());
+ }
+
+ @Test
+ void shouldThrowWhenToManyGroupsAssigned() {
+ Set longStudentGroups = IntStream.range(0, 101)
+ .mapToObj(i -> StudentGroup.builder().name("group" + i).build())
+ .collect(Collectors.toSet());
+
+ assertThrows(UnsupportedCountOfArgumentsException.class, () -> Exam.builder()
+ .title("title")
+ .description("description")
+ .examDate(date)
+ .examType(examType)
+ .groups(longStudentGroups)
+ .build());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java
new file mode 100644
index 0000000..52490f4
--- /dev/null
+++ b/src/test/java/org/pkwmtt/examCalendar/repository/ExamRepositoryTest.java
@@ -0,0 +1,177 @@
+package org.pkwmtt.examCalendar.repository;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.pkwmtt.examCalendar.entity.Exam;
+import org.pkwmtt.examCalendar.entity.ExamType;
+import org.pkwmtt.examCalendar.entity.StudentGroup;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Slf4j
+@DataJpaTest
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
+@ActiveProfiles("database")
+class ExamRepositoryTest {
+
+ @Autowired
+ private ExamRepository examRepository;
+
+ @Autowired
+ private ExamTypeRepository examTypeRepository;
+
+ @Autowired
+ private GroupRepository groupRepository;
+
+ @BeforeAll
+ void setUp() {
+ ExamType examType = ExamType.builder()
+ .name("exam").build();
+ examTypeRepository.save(examType);
+
+ StudentGroup g12A = StudentGroup.builder()
+ .name("12A").build();
+ StudentGroup g12A1 = StudentGroup.builder()
+ .name("12A1").build();
+ StudentGroup g12A2 = StudentGroup.builder()
+ .name("12A2").build();
+
+ StudentGroup g12K = StudentGroup.builder()
+ .name("12K").build();
+ StudentGroup g12K1 = StudentGroup.builder()
+ .name("12K1").build();
+ StudentGroup g12K2 = StudentGroup.builder()
+ .name("12K2").build();
+ StudentGroup g12K3 = StudentGroup.builder()
+ .name("12K3").build();
+ StudentGroup gL04 = StudentGroup.builder()
+ .name("L04").build();
+ StudentGroup gL05 = StudentGroup.builder()
+ .name("L05").build();
+
+ groupRepository.save(g12A);
+ groupRepository.save(g12A1);
+ groupRepository.save(g12A2);
+
+ groupRepository.save(g12K);
+ groupRepository.save(g12K1);
+ groupRepository.save(g12K2);
+ groupRepository.save(g12K3);
+ groupRepository.save(gL04);
+ groupRepository.save(gL05);
+
+ Exam smallGroupExam1 = Exam.builder()
+ .title("small Group Exam 1")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(g12K, gL04))
+ .build();
+
+ Exam smallGroupExam2 = Exam.builder()
+ .title("small Group Exam 2")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(gL04, g12K, gL05))
+ .build();
+
+ Exam smallGroupExam3 = Exam.builder()
+ .title("small Group Exam 3")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(g12A, gL05))
+ .build();
+
+ Exam generalGroupExam1 = Exam.builder()
+ .title("general Group Exam 1")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(g12K1, g12K2, g12K3))
+ .build();
+
+ Exam generalGroupExam2 = Exam.builder()
+ .title("general Group Exam 2")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(g12K1))
+ .build();
+
+ Exam generalGroupExam3 = Exam.builder()
+ .title("general Group Exam 3")
+ .description("Linear Algebra")
+ .examDate(LocalDateTime.now().plusDays(1))
+ .examType(examType)
+ .groups(Set.of(g12A1, g12A2))
+ .build();
+
+ examRepository.save(smallGroupExam1);
+ examRepository.save(smallGroupExam2);
+ examRepository.save(smallGroupExam3);
+ examRepository.save(generalGroupExam1);
+ examRepository.save(generalGroupExam2);
+ examRepository.save(generalGroupExam3);
+ }
+
+ @Test
+ void shouldReturnExamsWhenNotAllSubgroupsFromRepositoryMatched() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L04"));
+ assertEquals(2, exams.size());
+ List examTitles = exams.stream().map(Exam::getTitle).sorted().toList();
+ assertEquals("small Group Exam 1", examTitles.get(0));
+ assertEquals("small Group Exam 2", examTitles.get(1));
+ }
+
+ @Test
+ void shouldReturnExamWhenNotAllSubgroupsFromArgumentsMatchedAndNotReturnExamsForWrongGeneralGroup() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L05"));
+ assertEquals(1, exams.size());
+ List examTitles = exams.stream().map(Exam::getTitle).sorted().toList();
+ assertEquals("small Group Exam 2", examTitles.getFirst());
+ }
+
+ @Test
+ void shouldReturnExamsWhenMultipleArgumentsMatch() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of("L04", "L05"));
+ assertEquals(2, exams.size());
+ Set examTitles = exams.stream().map(Exam::getTitle).collect(Collectors.toSet());
+ assertTrue(examTitles.contains("small Group Exam 1"));
+ assertTrue(examTitles.contains("small Group Exam 2"));
+ }
+
+ @Test
+ void ShouldReturnEmptyListWhenSubgroupsSetIsEmpty() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K", Set.of());
+ assertTrue(exams.isEmpty());
+ }
+
+ @Test
+ void shouldReturnEmptyListWhenGeneralGroupIdentifierHasInvalidFormat() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12K2", Set.of("L04"));
+ assertTrue(exams.isEmpty());
+ }
+
+ @Test
+ void shouldReturnEmptyListWhenGeneralGroupIdentifierDontMatch() {
+ Set exams = examRepository.findAllBySubgroupsOfGeneralGroup("12B", Set.of("L04", "L05"));
+ assertTrue(exams.isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/otp/OTPServiceTest.java b/src/test/java/org/pkwmtt/otp/OTPServiceTest.java
new file mode 100644
index 0000000..9fa98fc
--- /dev/null
+++ b/src/test/java/org/pkwmtt/otp/OTPServiceTest.java
@@ -0,0 +1,156 @@
+package org.pkwmtt.otp;
+
+import com.icegreen.greenmail.configuration.GreenMailConfiguration;
+import com.icegreen.greenmail.junit5.GreenMailExtension;
+import com.icegreen.greenmail.util.ServerSetupTest;
+import com.mysql.cj.exceptions.WrongArgumentException;
+import jakarta.mail.Multipart;
+import jakarta.mail.Part;
+import jakarta.mail.internet.MimeMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.pkwmtt.exceptions.OTPCodeNotFoundException;
+import org.pkwmtt.exceptions.SpecifiedGeneralGroupDoesntExistsException;
+import org.pkwmtt.exceptions.WrongOTPFormatException;
+import org.pkwmtt.otp.dto.OTPRequest;
+import org.pkwmtt.otp.repository.OTPCodeRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Slf4j
+@TestInstance(TestInstance.Lifecycle.PER_METHOD)
+@ActiveProfiles("database")
+@SpringBootTest
+@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
+class OTPServiceTest {
+
+ @Autowired
+ private OTPService otpService;
+
+ @Autowired
+ private OTPCodeRepository otpCodeRepository;
+
+ @RegisterExtension
+ static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
+ .withConfiguration(GreenMailConfiguration.aConfig().withUser("test@localhost", "test"))
+ .withPerMethodLifecycle(true);
+
+ @Test
+ void shouldSendCorrectMailWithRepresentativePayload () {
+ //given
+ List requests = List.of(new OTPRequest("test@localhost", "12K"));
+ Pattern pattern = Pattern.compile("[A-Z0-9]{6}");
+ //when
+ otpService.sendOTPCodesForManyGroups(requests);
+
+ //then
+ assertAll(() -> {
+ assertTrue(greenMail.waitForIncomingEmail(1));
+
+ MimeMessage receivedMessage = greenMail.getReceivedMessages()[0];
+ assertEquals("Kod Starosty 12K", receivedMessage.getSubject());
+ assertEquals("test@localhost", receivedMessage.getAllRecipients()[0].toString());
+
+ Matcher matcher = pattern.matcher(Objects.requireNonNull(extractBody(receivedMessage)));
+ assertTrue(matcher.find());
+ System.out.println(matcher.group(0));
+ assertTrue(otpCodeRepository.existsOTPCodeByCode(matcher.group(0)));
+ });
+ }
+
+ @Test
+ void shouldThrow_WrongArgumentException () {
+ //given
+ List requests = List.of(new OTPRequest("test@localhost", "12K1"));
+ //when
+ //then
+ assertThrows(WrongArgumentException.class, () -> otpService.sendOTPCodesForManyGroups(requests));
+ }
+
+ @Test
+ void shouldThrow_SpecifiedGeneralGroupDoesntExistsException () {
+ //given
+ List requests = List.of(new OTPRequest("test@localhost", "XXXX"));
+ //when
+ //then
+ assertThrows(SpecifiedGeneralGroupDoesntExistsException.class, () -> otpService.sendOTPCodesForManyGroups(requests));
+ }
+
+ @Test
+ void shouldGenerateTokenForRepresentative () throws Exception {
+ //given
+ List requests = List.of(new OTPRequest("test@localhost", "12K"));
+ Pattern otpPattern = Pattern.compile("[A-Z0-9]{6}");
+ Pattern tokenPattern = Pattern.compile("[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+");
+
+ //when
+ otpService.sendOTPCodesForManyGroups(requests); //generate mail with code
+ greenMail.waitForIncomingEmail(1); // fetch mail
+
+ MimeMessage receivedMessage = greenMail.getReceivedMessages()[0];
+ Matcher otpMatcher = otpPattern.matcher(Objects.requireNonNull(extractBody(receivedMessage))); //get content
+
+ final String code;
+ if (otpMatcher.find()) {
+ code = otpMatcher.group();
+ } else {
+ code = "";
+ fail("Code not found");
+ }
+
+ String token = otpService.generateTokenForRepresentative(code); //generate token
+
+ //then
+ assertAll(() -> {
+ assertNotNull(token);
+
+ Matcher tokenMatcher = tokenPattern.matcher(token);
+ assertTrue(tokenMatcher.find());
+ assertFalse(otpCodeRepository.existsOTPCodeByCode(code));
+ });
+ }
+
+ @Test
+ void shouldThrow_WrongOTPFormatException_wrongCharacters () {
+ assertThrows(WrongOTPFormatException.class, () -> otpService.generateTokenForRepresentative("XXXXX#"));
+ }
+
+ @Test
+ void shouldThrow_WrongOTPFormatException_tooLongCode () {
+ assertThrows(WrongOTPFormatException.class, () -> otpService.generateTokenForRepresentative("X".repeat(7)));
+ }
+
+ @Test
+ void shouldThrow_OTPCodeNotFoundException () {
+ assertThrows(OTPCodeNotFoundException.class, () -> otpService.generateTokenForRepresentative("X".repeat(6)));
+ }
+
+ private String extractBody (Part part) throws Exception {
+ if (part.isMimeType("text/plain") || part.isMimeType("text/html")) {
+ return (String) part.getContent();
+ }
+ if (part.isMimeType("multipart/*")) {
+ Multipart mp = (Multipart) part.getContent();
+ for (int i = 0; i < mp.getCount(); i++) {
+ String result = extractBody(mp.getBodyPart(i));
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/pkwmtt/pkwmttbackend/PkwmttBackendApplicationTests.java b/src/test/java/org/pkwmtt/pkwmttbackend/PkwmttBackendApplicationTests.java
deleted file mode 100644
index 2ae95fd..0000000
--- a/src/test/java/org/pkwmtt/pkwmttbackend/PkwmttBackendApplicationTests.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.pkwmtt.pkwmttbackend;
-
-import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-
-@SpringBootTest
-class PkwmttBackendApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/src/test/java/org/pkwmtt/security/token/JwtServiceImplTest.java b/src/test/java/org/pkwmtt/security/token/JwtServiceImplTest.java
new file mode 100644
index 0000000..9c50238
--- /dev/null
+++ b/src/test/java/org/pkwmtt/security/token/JwtServiceImplTest.java
@@ -0,0 +1,139 @@
+package org.pkwmtt.security.token;
+
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.pkwmtt.examCalendar.entity.User;
+import org.pkwmtt.examCalendar.enums.Role;
+import org.pkwmtt.security.token.dto.UserDTO;
+import org.pkwmtt.security.token.utils.JwtUtils;
+
+import java.util.Base64;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class JwtServiceImplTest {
+
+ private JwtServiceImpl jwtService;
+
+ @BeforeEach
+ void setUp() {
+ JwtUtils jwtUtils = mock(JwtUtils.class);
+
+ byte[] keyBytes = new byte[32];
+ for (int i = 0; i < 32; i++) keyBytes[i] = (byte) i;
+ String secretBase64 = Base64.getEncoder().encodeToString(keyBytes);
+
+ when(jwtUtils.getSecret()).thenReturn(secretBase64);
+ when(jwtUtils.getExpirationMs()).thenReturn(1000L * 60 * 60 * 24 * 30 * 6);
+
+ jwtService = new JwtServiceImpl(jwtUtils);
+ }
+
+ @Test
+ void generateToken_shouldCreateNonEmptyToken() {
+ UserDTO user = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(user);
+ assertNotNull(token);
+ assertFalse(token.isEmpty());
+ }
+
+ @Test
+ void getUserEmailFromToken_shouldReturnCorrectEmail() {
+ UserDTO user = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(user);
+ String email = jwtService.getUserEmailFromToken(token);
+ assertEquals("user@example.com", email);
+ }
+
+ @Test
+ void extractRoleFromToken_shouldReturnCorrectRole() {
+ UserDTO user = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(user);
+ String roleClaim = jwtService.extractClaim(token, claims -> claims.get("role", String.class));
+ assertEquals("ADMIN", roleClaim);
+ }
+
+ @Test
+ void extractGroupFromToken_shouldReturnCorrectGroup() {
+ UserDTO user = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(user);
+ String groupClaim = jwtService.extractClaim(token, claims -> claims.get("group", String.class));
+ assertEquals("GROUP1", groupClaim);
+ }
+
+ @Test
+ void validateToken_shouldReturnTrueForValidToken() {
+ UserDTO userDTO = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(userDTO);
+ User mockUser = mock(User.class);
+ when(mockUser.getEmail()).thenReturn("user@example.com");
+ assertTrue(jwtService.validateToken(token, mockUser));
+ }
+
+ @Test
+ void validateToken_shouldReturnFalseForInvalidEmail() {
+ UserDTO userDTO = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ String token = jwtService.generateToken(userDTO);
+ User mockUser = mock(User.class);
+ when(mockUser.getEmail()).thenReturn("other@example.com");
+ assertFalse(jwtService.validateToken(token, mockUser));
+ }
+
+ @Test
+ void validateToken_shouldReturnFalseForExpiredToken() {
+ UserDTO user = new UserDTO()
+ .setEmail("user@example.com")
+ .setGroup("GROUP1")
+ .setRole(Role.ADMIN);
+
+ long pastExpiration = System.currentTimeMillis() - 1000;
+ String expiredToken = Jwts.builder()
+ .subject(user.getEmail())
+ .claim("group", user.getGroup())
+ .claim("role", user.getRole())
+ .issuedAt(new Date(System.currentTimeMillis() - 2000))
+ .expiration(new Date(pastExpiration))
+ .signWith(jwtService.decodeSecretKey())
+ .compact();
+
+ User mockUser = mock(User.class);
+ when(mockUser.getEmail()).thenReturn("user@example.com");
+
+ assertFalse(jwtService.validateToken(expiredToken, mockUser));
+ }
+
+ @Test
+ void getUserEmailFromToken_shouldThrowExceptionForInvalidToken() {
+ String invalidToken = "invalid.token.value";
+ assertThrows(JwtException.class, () -> jwtService.getUserEmailFromToken(invalidToken));
+ }
+}
diff --git a/src/test/java/org/pkwmtt/security/token/filter/JwtFilterTest.java b/src/test/java/org/pkwmtt/security/token/filter/JwtFilterTest.java
new file mode 100644
index 0000000..54123dc
--- /dev/null
+++ b/src/test/java/org/pkwmtt/security/token/filter/JwtFilterTest.java
@@ -0,0 +1,58 @@
+package org.pkwmtt.security.token.filter;
+
+import jakarta.servlet.FilterChain;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.pkwmtt.examCalendar.entity.User;
+import org.pkwmtt.examCalendar.enums.Role;
+import org.pkwmtt.examCalendar.repository.UserRepository;
+import org.pkwmtt.security.token.JwtService;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class JwtFilterTest {
+
+ private JwtService jwtService;
+ private UserRepository userRepository;
+ private JwtFilter jwtFilter;
+
+ @BeforeEach
+ void setUp() {
+ jwtService = mock(JwtService.class);
+ userRepository = mock(UserRepository.class);
+ jwtFilter = new JwtFilter();
+ jwtFilter.jwtService = jwtService;
+ jwtFilter.userRepository = userRepository;
+
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void givenValidToken_whenDoFilter_thenAuthenticationSet() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addHeader("Authorization", "Bearer validToken");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ FilterChain filterChain = mock(FilterChain.class);
+
+ User mockUser = mock(User.class);
+ when(mockUser.getRole()).thenReturn(Role.valueOf("ADMIN"));
+ when(mockUser.getEmail()).thenReturn("user@example.com");
+
+ when(jwtService.getUserEmailFromToken("validToken")).thenReturn("user@example.com");
+ when(jwtService.validateToken(eq("validToken"), any(User.class))).thenReturn(true);
+ when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(mockUser));
+
+ jwtFilter.doFilterInternal(request, response, filterChain);
+
+ assertNotNull(SecurityContextHolder.getContext().getAuthentication());
+ }
+}
diff --git a/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java
index f25888f..bef3add 100644
--- a/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java
+++ b/src/test/java/org/pkwmtt/timetable/TimetableCacheServiceTest.java
@@ -2,47 +2,40 @@
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.pkwmtt.ValuesForTest;
import org.pkwmtt.cache.CacheInspector;
import org.pkwmtt.timetable.dto.TimetableDTO;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
import test.TestConfig;
-import java.util.List;
import java.util.Map;
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
+@SpringBootTest
class TimetableCacheServiceTest extends TestConfig {
@Autowired
TimetableCacheService cachedService;
- @Autowired
- TimetableService service;
-
@Autowired
CacheInspector cacheInspector;
-
+
@BeforeEach
- public void initWireMock() {
- EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html"))
- .willReturn(aResponse()
- .withStatus(200)
- .withHeader("Content-Type", "text/*")
- .withBody(ValuesForTest.timetableHTML)));
-
- EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html"))
- .willReturn(aResponse()
- .withStatus(200)
- .withHeader("Content-Type", "text/*")
- .withBody(ValuesForTest.listHTML)));
+ public void initWireMock () {
+ EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/plany/o25.html")).willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/*")
+ .withBody(ValuesForTest.timetableHTML)));
+
+ EXTERNAL_SERVICE_API_MOCK.stubFor(get(urlPathMatching("/lista.html")).willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "text/*")
+ .withBody(ValuesForTest.listHTML)));
}
@Test
@@ -56,13 +49,12 @@ public void shouldHourListBePresentInCache () {
//then
assertAll(
- () -> assertNotNull(cacheData),
- () -> assertTrue(cacheData.containsKey(key)),
- () -> {
- var hourList = cacheData.get(key);
- assertNotNull(hourList);
- assertThat(hourList).isEqualTo("[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]");
- }
+ () -> assertNotNull(cacheData), () -> assertTrue(cacheData.containsKey(key)), () -> {
+ var hourList = cacheData.get(key);
+ assertNotNull(hourList);
+ assertThat(hourList).isEqualTo(
+ "[\"7:30- 8:15\",\"8:15- 9:00\",\"9:15-10:00\",\"10:00-10:45\",\"11:00-11:45\",\"11:45-12:30\",\"12:45-13:30\",\"13:30-14:15\",\"14:30-15:15\",\"15:15-16:00\",\"16:15-17:00\",\"17:00-17:45\",\"18:00-18:45\",\"18:45-19:30\",\"19:45-20:30\",\"20:30-21:15\"]");
+ }
);
}
@@ -71,16 +63,21 @@ public void shouldHourListBePresentInCache () {
public void shouldReturnGeneralGroupsMap () {
//given
var expectedMap = Map.of(
- "11K2", "plany/o8.html",
- "12K1", "plany/o25.html",
- "11A1", "plany/o1.html",
- "12K3", "plany/o27.html",
- "12K2", "plany/o26.html"
+ "11K2",
+ "plany/o8.html",
+ "12K1",
+ "plany/o25.html",
+ "11A1",
+ "plany/o1.html",
+ "12K3",
+ "plany/o27.html",
+ "12K2",
+ "plany/o26.html"
);
-
+
//when
var result = cachedService.getGeneralGroupsMap();
-
+
//then
assertThat(result).isEqualTo(expectedMap);
}
@@ -94,24 +91,21 @@ public void shouldGeneralGroupMapBePresentInCache () {
//when
Map