Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
25 changes: 25 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
Expand All @@ -290,6 +298,23 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<configuration>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
<verbose>false</verbose>
<dateFormat>yyyy-MM-dd'T'HH:mm:ssXXX</dateFormat>
<dateFormatTimeZone>UTC</dateFormatTimeZone>
<includeOnlyProperties>
<includeOnlyProperty>^git.branch$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.id$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.time$</includeOnlyProperty>
</includeOnlyProperties>
</configuration>
</plugin>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public SecurityFilterChain configure(HttpSecurity http) {
"/api/v1/user-activation/verify",
"/ui",
"/ui/**",
"/uploads/news/**")
"/uploads/news/**",
"/api/v1/version")
.permitAll()
.requestMatchers(
"/index.html",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.itasocialacademy.oitassist.version.api;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;

@Schema(description = "Build version of the running application")
public record VersionResponse(
@Schema(description = "Backend build information") BackendVersion backend,
@Schema(description = "Date and time when the artifact was built") Instant buildTime) {
@Schema(description = "Backend build information")
public record BackendVersion(
@Schema(
description = "Full hash of the commit the artifact was built from",
example = "3534bab24568a602605078b9264711223f218dd2") String commitId,
@Schema(
description = "Short hash of the commit the artifact was built from",
example = "3534bab") String shortCommitId,
@Schema(description = "Date and time of the commit") Instant commitTime,
@Schema(description = "Branch the artifact was built from", example = "dev") String branch,
@Schema(description = "Artifact version", example = "0.0.1-SNAPSHOT") String version) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.itasocialacademy.oitassist.version.controller;

import com.itasocialacademy.oitassist.version.api.VersionResponse;
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/version")
@Tag(name = "Version", description = "Public API for the build version of the running application")
public class VersionController {
private final VersionService versionService;

@GetMapping
@Operation(
summary = "Get application build version",
description = """
Returns the build version of the running application: the commit the backend
was built from and the date the artifact was built.
Values that were not available at build time are returned as null.
Publicly accessible, no authentication required.
""")
@ApiResponse(
responseCode = "200",
description = "Build version retrieved successfully",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = VersionResponse.class)))
public VersionResponse getVersion() {
return versionService.getVersion();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@org.springframework.modulith.ApplicationModule(
displayName = "version",
allowedDependencies = {})

package com.itasocialacademy.oitassist.version;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.itasocialacademy.oitassist.version.service;

import com.itasocialacademy.oitassist.version.api.VersionResponse;
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.stereotype.Service;
import java.time.Instant;

@Slf4j
@Service
public class VersionServiceImpl implements VersionService {
private final GitProperties gitProperties;
private final BuildProperties buildProperties;

public VersionServiceImpl(
ObjectProvider<@NonNull GitProperties> gitPropertiesProvider,
ObjectProvider<@NonNull BuildProperties> buildPropertiesProvider) {
this.gitProperties = gitPropertiesProvider.getIfAvailable();
this.buildProperties = buildPropertiesProvider.getIfAvailable();

if (this.gitProperties == null) {
log.warn("git.properties is missing, commit data will not be reported");
}
if (this.buildProperties == null) {
log.warn("build-info.properties is missing, build data will not be reported");
}
}

@Override
public VersionResponse getVersion() {
return new VersionResponse(getBackendVersion(), getBuildTime());
}

private VersionResponse.BackendVersion getBackendVersion() {
if (gitProperties == null) {
return new VersionResponse.BackendVersion(null, null, null, null, null);
}
return new VersionResponse.BackendVersion(
gitProperties.getCommitId(),
gitProperties.getShortCommitId(),
gitProperties.getCommitTime(),
gitProperties.getBranch(),
getArtifactVersion());
}

private String getArtifactVersion() {
return buildProperties == null ? null : buildProperties.getVersion();
}

private Instant getBuildTime() {
return buildProperties == null ? null : buildProperties.getTime();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.itasocialacademy.oitassist.version.service.interfaces;

import com.itasocialacademy.oitassist.version.api.VersionResponse;

public interface VersionService {
VersionResponse getVersion();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.itasocialacademy.oitassist.version.controller;

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

import com.itasocialacademy.oitassist.core.web.AppExceptionHttpStatusMapper;
import com.itasocialacademy.oitassist.security.jwt.JwtFilter;
import com.itasocialacademy.oitassist.version.api.VersionResponse;
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(
controllers = VersionController.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = JwtFilter.class))
@Import(VersionControllerTest.SecurityTestConfiguration.class)
class VersionControllerTest {

private static final String ENDPOINT = "/api/v1/version";
private static final String COMMIT_ID = "3534bab24568a602605078b9264711223f218dd2";
private static final String SHORT_COMMIT_ID = "3534bab";
private static final String COMMIT_TIME = "2026-08-19T08:22:05Z";
private static final String BUILD_TIME = "2026-08-19T19:55:29.490Z";

@Autowired
private MockMvc mockMvc;

@MockitoBean
private VersionService versionService;

@MockitoBean
private AppExceptionHttpStatusMapper appExceptionHttpStatusMapper;

@Test
void getVersion_ShouldReturnVersion_WhenCallerIsAnonymous() throws Exception {
when(versionService.getVersion()).thenReturn(
new VersionResponse(
new VersionResponse.BackendVersion(
COMMIT_ID,
SHORT_COMMIT_ID,
Instant.parse(COMMIT_TIME),
"dev",
"0.0.1-SNAPSHOT"),
Instant.parse(BUILD_TIME)));

mockMvc.perform(get(ENDPOINT))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.backend.commitId").value(COMMIT_ID))
.andExpect(jsonPath("$.backend.shortCommitId").value(SHORT_COMMIT_ID))
.andExpect(jsonPath("$.backend.commitTime").value(COMMIT_TIME))
.andExpect(jsonPath("$.backend.branch").value("dev"))
.andExpect(jsonPath("$.backend.version").value("0.0.1-SNAPSHOT"))
.andExpect(jsonPath("$.buildTime").value(BUILD_TIME));
}

@Test
void getVersion_ShouldReturnOkWithEmptyValues_WhenBuildMetadataIsMissing() throws Exception {
when(versionService.getVersion()).thenReturn(
new VersionResponse(
new VersionResponse.BackendVersion(null, null, null, null, null),
null));

mockMvc.perform(get(ENDPOINT))
.andExpect(status().isOk())
.andExpect(jsonPath("$.backend").exists())
.andExpect(jsonPath("$.backend.commitId").isEmpty())
.andExpect(jsonPath("$.backend.commitTime").isEmpty())
.andExpect(jsonPath("$.backend.branch").isEmpty())
.andExpect(jsonPath("$.buildTime").isEmpty());
}

@TestConfiguration(proxyBeanMethods = false)
@EnableWebSecurity
static class SecurityTestConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorization -> authorization
.anyRequest()
.permitAll())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.itasocialacademy.oitassist.version.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import com.itasocialacademy.oitassist.version.api.VersionResponse;
import java.time.Instant;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.info.GitProperties;

@ExtendWith(MockitoExtension.class)
class VersionServiceImplTest {

private static final String COMMIT_ID = "3534bab24568a602605078b9264711223f218dd2";
private static final String SHORT_COMMIT_ID = "3534bab";
private static final String BRANCH = "dev";
private static final String ARTIFACT_VERSION = "0.0.1-SNAPSHOT";
private static final Instant COMMIT_TIME = Instant.parse("2026-08-19T08:22:05Z");
private static final Instant BUILD_TIME = Instant.parse("2026-08-19T19:55:29.490Z");

@Mock
private ObjectProvider<GitProperties> gitPropertiesProvider;

@Mock
private ObjectProvider<BuildProperties> buildPropertiesProvider;

@Test
void getVersion_ShouldReturnFullVersion_WhenBuildMetadataIsPresent() {
when(gitPropertiesProvider.getIfAvailable()).thenReturn(gitProperties());
when(buildPropertiesProvider.getIfAvailable()).thenReturn(buildProperties());

VersionResponse version = createService().getVersion();

assertThat(version.buildTime()).isEqualTo(BUILD_TIME);
assertThat(version.backend().commitId()).isEqualTo(COMMIT_ID);
assertThat(version.backend().shortCommitId()).isEqualTo(SHORT_COMMIT_ID);
assertThat(version.backend().commitTime()).isEqualTo(COMMIT_TIME);
assertThat(version.backend().branch()).isEqualTo(BRANCH);
assertThat(version.backend().version()).isEqualTo(ARTIFACT_VERSION);
}

@Test
void getVersion_ShouldReturnEmptyValues_WhenBuildMetadataIsMissing() {
VersionResponse version = createService().getVersion();

assertThat(version.buildTime()).isNull();
assertThat(version.backend()).isNotNull();
assertThat(version.backend().commitId()).isNull();
assertThat(version.backend().shortCommitId()).isNull();
assertThat(version.backend().commitTime()).isNull();
assertThat(version.backend().branch()).isNull();
assertThat(version.backend().version()).isNull();
}

@Test
void getVersion_ShouldReturnCommitDataOnly_WhenBuildInfoIsMissing() {
when(gitPropertiesProvider.getIfAvailable()).thenReturn(gitProperties());

VersionResponse version = createService().getVersion();

assertThat(version.backend().commitId()).isEqualTo(COMMIT_ID);
assertThat(version.backend().commitTime()).isEqualTo(COMMIT_TIME);
assertThat(version.backend().branch()).isEqualTo(BRANCH);
assertThat(version.backend().version()).isNull();
assertThat(version.buildTime()).isNull();
}

private VersionServiceImpl createService() {
return new VersionServiceImpl(gitPropertiesProvider, buildPropertiesProvider);
}

private GitProperties gitProperties() {
Properties entries = new Properties();
entries.setProperty("branch", BRANCH);
entries.setProperty("commit.id", COMMIT_ID);
entries.setProperty("commit.id.abbrev", SHORT_COMMIT_ID);
entries.setProperty("commit.time", COMMIT_TIME.toString());
return new GitProperties(entries);
}

private BuildProperties buildProperties() {
Properties entries = new Properties();
entries.setProperty("group", "com.ita-social-academy");
entries.setProperty("artifact", "OITAssist");
entries.setProperty("version", ARTIFACT_VERSION);
entries.setProperty("time", BUILD_TIME.toString());
return new BuildProperties(entries);
}
}
Loading