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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions chat/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<jquery>3.1.1-1</jquery>
<jjwt.version>0.12.3</jjwt.version>
<checkstyle-maven-plugin.version>3.3.1</checkstyle-maven-plugin.version>
<org.eclipse.jgit.version>7.1.0.202411261347-r</org.eclipse.jgit.version>
<json-path.version>2.9.0</json-path.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -175,6 +177,17 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${org.eclipse.jgit.version}</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json-path.version}</version> <!-- Use the latest stable version -->
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
4 changes: 3 additions & 1 deletion chat/src/main/java/greencity/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

/**
* Config for security.
*
*/
@Configuration
@EnableWebSecurity
Expand Down Expand Up @@ -107,6 +106,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/webjars/**",
"/chat/**")
.permitAll()
.requestMatchers(HttpMethod.GET,
"/commit-info")
.permitAll()
.requestMatchers(HttpMethod.GET,
"/chat/create-chatRoom",
"/chat/messages/{room_id}",
Expand Down
2 changes: 2 additions & 0 deletions chat/src/main/java/greencity/constant/AppConstant.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package greencity.constant;

public final class AppConstant {
public static final String UKRAINE_TIMEZONE = "Europe/Kyiv";
public static final String DATE_FORMAT = "dd/MM/yyyy HH:mm:ss";
public static final String ADMIN = "ADMIN";
public static final String MODERATOR = "MODERATOR";
public static final String USER = "USER";
Expand Down
5 changes: 5 additions & 0 deletions chat/src/main/java/greencity/constant/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public final class ErrorMessage {
public static final String USER_NOT_THE_OWNER = "This user is not the owner of the chat";
public static final String CHAT_MESSAGE_NOT_FOUND_BY_ID = "Chat message not found by id: ";
public static final String CHAT_MESSAGE_CANNOT_BE_EMPTY = "Message content cannot be empty!";
public static final String WARNING_GIT_DIRECTORY_NOT_FOUND =
"WARNING: .git directory not found. Git commit info will be unavailable.";
public static final String GIT_REPOSITORY_NOT_INITIALIZED =
"Git repository not initialized. Commit info is unavailable.";
public static final String FAILED_TO_FETCH_COMMIT_INFO = "Failed to fetch commit info due to I/O error: ";

private ErrorMessage() {
}
Expand Down
56 changes: 56 additions & 0 deletions chat/src/main/java/greencity/controller/CommitInfoController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package greencity.controller;

import greencity.constant.HttpStatuses;
import greencity.dto.CommitInfoDto;
import greencity.service.CommitInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
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;

/**
* Controller for fetching Git commit information.
*/
@RestController
@RequestMapping("/commit-info")
@RequiredArgsConstructor
public class CommitInfoController {
Comment on lines +19 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding security measures.

The endpoint is publicly accessible. Consider:

  1. Adding authentication/authorization checks
  2. Implementing rate limiting to prevent abuse
 @RestController
 @RequestMapping("/commit-info")
 @RequiredArgsConstructor
+@SecurityRequirement(name = "Bearer Authentication")
 public class CommitInfoController {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@RestController
@RequestMapping("/commit-info")
@RequiredArgsConstructor
public class CommitInfoController {
@RestController
@RequestMapping("/commit-info")
@RequiredArgsConstructor
@SecurityRequirement(name = "Bearer Authentication")
public class CommitInfoController {

private final CommitInfoService commitInfoService;

/**
* Endpoint to fetch the latest Git commit hash and date.
*
* @return {@link CommitInfoDto}
*/
@Operation(summary = "Get the latest commit hash and date.")
@ApiResponse(
responseCode = "200",
description = HttpStatuses.OK,
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"commitHash": "d6e70c46b39857846f3f13ca9756c39448ab3d6f",
"commitDate": "16/12/2024 10:55:00"
}""")))
@ApiResponse(
responseCode = "404",
description = HttpStatuses.NOT_FOUND,
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"message": "Git repository not initialized. Commit info is unavailable."
}""")))
@GetMapping
public ResponseEntity<CommitInfoDto> getCommitInfo() {
return ResponseEntity.ok(commitInfoService.getLatestCommitInfo());
}
Comment on lines +53 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add explicit error handling.

The current implementation might throw uncaught exceptions. Consider:

  1. Adding try-catch block to handle potential service exceptions
  2. Using @ExceptionHandler or global exception handler
     @GetMapping
     public ResponseEntity<CommitInfoDto> getCommitInfo() {
-        return ResponseEntity.ok(commitInfoService.getLatestCommitInfo());
+        try {
+            return ResponseEntity.ok(commitInfoService.getLatestCommitInfo());
+        } catch (ResourceNotFoundException e) {
+            throw e;  // Let global exception handler handle it
+        } catch (Exception e) {
+            log.error("Unexpected error while fetching commit info", e);
+            throw new ServiceException("Failed to fetch commit information");
+        }
     }

Committable suggestion skipped: line range outside the PR's diff.

}
21 changes: 21 additions & 0 deletions chat/src/main/java/greencity/dto/CommitInfoDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package greencity.dto;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
* DTO for commit information response.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
Copy link

@dxrknesss dxrknesss Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can drop @EqualsAndHashCode, it's included in @Data

https://projectlombok.org/features/Data

public class CommitInfoDto {
@NotNull

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could annotate with @NotEmpty as well (if business logic dictates so)

private String commitHash;
@NotNull
private String commitDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package greencity.exception.exceptions;

import lombok.experimental.StandardException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@StandardException
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import greencity.exception.exceptions.VoiceMessageNotFoundException;
import greencity.exception.exceptions.UserIsNotAdminException;
import greencity.exception.exceptions.TariffNotFoundException;
import greencity.exception.exceptions.ResourceNotFoundException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.error.ErrorAttributeOptions;
Expand Down Expand Up @@ -129,4 +130,23 @@ public final ResponseEntity<Object> handleTariffNotFoundException(TariffNotFound
log.trace(ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionResponse);
}

/**
* Method intercepts exception {@link ResourceNotFoundException}.
*
* @param ex Exception that should be intercepted.
* @param request Contains details about the occurred exception.
* @return {@code ResponseEntity} which contains the HTTP status and body with
* the exception message.
*/
@ExceptionHandler(ResourceNotFoundException.class)
public final ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if ReponseEntity is the best return type. there are specific types for error in spring, you can look here:

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/return-types.html

WebRequest request) {
log.error(ex.getMessage(), ex);

ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request));
exceptionResponse.setMessage(ex.getMessage());

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionResponse);
}
}
15 changes: 15 additions & 0 deletions chat/src/main/java/greencity/service/CommitInfoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package greencity.service;

import greencity.dto.CommitInfoDto;

/**
* Interface for fetching Git commit information.
*/
public interface CommitInfoService {
/**
* Fetches the latest Git commit hash and date.
*
* @return {@link CommitInfoDto}
*/
CommitInfoDto getLatestCommitInfo();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package greencity.service.impl;

import greencity.constant.AppConstant;
import greencity.constant.ErrorMessage;
import greencity.dto.CommitInfoDto;
import greencity.exception.exceptions.ResourceNotFoundException;
import greencity.service.CommitInfoService;
import java.io.File;
import java.io.IOException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.springframework.stereotype.Service;

/**
* Service implementation for fetching Git commit information.
*/
@Service
@Slf4j
public class CommitInfoServiceImpl implements CommitInfoService {
private Repository repository;

private static final String COMMIT_REF = "HEAD";

public CommitInfoServiceImpl() {
try {
repository = new FileRepositoryBuilder()
.setGitDir(new File(".git"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could export ".git" to constant

.setMustExist(true)
.readEnvironment()
.findGitDir()
.build();
} catch (IOException e) {
repository = null;
log.warn(ErrorMessage.WARNING_GIT_DIRECTORY_NOT_FOUND);
}
}

/**
* {@inheritDoc}
*
* @throws ResourceNotFoundException if the Git repository cannot be found, is
* not initialized, or commit information
* cannot be fetched due to an I/O error.
*/
@Override
public CommitInfoDto getLatestCommitInfo() {
if (repository == null) {
throw new ResourceNotFoundException(ErrorMessage.GIT_REPOSITORY_NOT_INITIALIZED);
}

try (RevWalk revWalk = new RevWalk(repository)) {
RevCommit latestCommit = revWalk.parseCommit(repository.resolve(COMMIT_REF));
String latestCommitHash = latestCommit.name();
String latestCommitDate = DateTimeFormatter.ofPattern(AppConstant.DATE_FORMAT)
.withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE))
.format(latestCommit.getAuthorIdent().getWhenAsInstant());

return new CommitInfoDto(latestCommitHash, latestCommitDate);
} catch (IOException e) {
throw new ResourceNotFoundException(ErrorMessage.FAILED_TO_FETCH_COMMIT_INFO + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package greencity.controller;

import greencity.dto.CommitInfoDto;
import greencity.exception.exceptions.ResourceNotFoundException;
import greencity.service.CommitInfoService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@ExtendWith(MockitoExtension.class)
class CommitInfoControllerTest {
@InjectMocks
private CommitInfoController commitInfoController;

@Mock
private CommitInfoService commitInfoService;

private MockMvc mockMvc;

@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(commitInfoController).build();
}

private static final String COMMIT_INFO_URL = "/commit-info";
private static final String COMMIT_HASH = "abc123";
private static final String COMMIT_DATE = "16/12/2024 12:06:32";
Comment on lines +39 to +41
Copy link

@dxrknesss dxrknesss Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally, test class should not be concerned about test data, I suggest you to move it to ModelUtils, or something like that..

+ less code in test class
+ less duplication of test data
- you have to look up entity data with lsp


@Test
void getCommitInfoReturnsSuccessTest() throws Exception {
CommitInfoDto commitInfoDto = new CommitInfoDto(COMMIT_HASH, COMMIT_DATE);
when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto);

mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.commitHash").value(COMMIT_HASH))
Copy link

@dxrknesss dxrknesss Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be better to convert object through objectmapper and compare it by .equals(), but it is to your preference

.andExpect(jsonPath("$.commitDate").value(COMMIT_DATE));

verify(commitInfoService, times(1)).getLatestCommitInfo();
}

@Test
void getCommitInfoReturnsErrorTest() throws Exception {
when(commitInfoService.getLatestCommitInfo()).thenThrow(new ResourceNotFoundException());

mockMvc.perform(get(COMMIT_INFO_URL))
.andExpect(status().isNotFound());

verify(commitInfoService, times(1)).getLatestCommitInfo();
}
}
Loading
Loading