6 Commits

Author SHA1 Message Date
almazlar
386f5137c6 fix: update Dockerfile, application properties, and controller mappings for health check and CORS support
All checks were successful
Release and Build Docker Images / release-and-build (push) Successful in 1m38s
2026-02-22 14:08:31 +03:00
almazlar
b5dcba1e11 fix: update application properties and Dockerfile for local development configuration 2026-02-22 13:18:15 +03:00
almazlar
854a0bace8 fix: update Dockerfile to run tests during build and adjust TodoController request mapping 2026-02-22 13:18:15 +03:00
almazlar
fda1f39901 fix: add CrossOrigin annotation to TodoController for CORS support 2026-02-22 13:18:15 +03:00
github-actions[bot]
73d0d767e9 chore(release): bump version to v0.0.7 and update changelog [skip ci] 2026-02-22 09:30:07 +00:00
almazlar
4050c08859 fix: update API base URL and add healthcheck for backend service
All checks were successful
Release and Build Docker Images / release-and-build (push) Successful in 1m30s
2026-02-22 12:29:55 +03:00
13 changed files with 55 additions and 25 deletions

View File

@@ -146,4 +146,4 @@ jobs:
labels: ${{ steps.meta-frontend.outputs.labels }} labels: ${{ steps.meta-frontend.outputs.labels }}
build-args: | build-args: |
VITE_APP_VERSION=${{ steps.generate.outputs.new_tag }} VITE_APP_VERSION=${{ steps.generate.outputs.new_tag }}
VITE_BASE_URL=https://todo.almazlar.com/api/todos VITE_BASE_URL=https://todo.almazlar.com/api

View File

@@ -1,3 +1,7 @@
## [v0.0.7] - 2026-02-22
* fix: update API base URL and add healthcheck for backend service (4050c08)
## [v0.0.6] - 2026-02-22 ## [v0.0.6] - 2026-02-22
* fix: update database connection URL and configure frontend build context (000d1db) * fix: update database connection URL and configure frontend build context (000d1db)

View File

@@ -1,15 +1,21 @@
# Build stage # Build stage ---------------------------------------------------------------
FROM maven:3.9.12-eclipse-temurin-25 AS build FROM maven:3.9.12-eclipse-temurin-25 AS build
WORKDIR /app WORKDIR /app
COPY pom.xml . COPY pom.xml .
COPY src ./src COPY src ./src
RUN mvn clean package -DskipTests RUN mvn clean package
# Run stage # Run stage ---------------------------------------------------------------
FROM eclipse-temurin:25-jre FROM eclipse-temurin:25-jre
WORKDIR /app WORKDIR /app
ARG APP_VERSION=dev ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} ENV APP_VERSION=${APP_VERSION}
# Install curl (required for the healthcheck)
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/*.jar app.jar COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080 EXPOSE 8082
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -65,6 +65,10 @@
<artifactId>spring-boot-starter-webmvc-test</artifactId> <artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -10,8 +10,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins("http://localhost:5173", "http://localhost:3000", "http://localhost:8081", .allowedOrigins("http://localhost:5173", "https://todo.almazlar.com")
"http://localhost:80", "https://todo.almazlar.com") // Typical Vite/React/Docker ports
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") .allowedHeaders("*")
.allowCredentials(true); .allowCredentials(true);

View File

@@ -11,7 +11,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
@RestController @RestController
@RequestMapping("/api/todos") @RequestMapping("/todos")
public class TodoController { public class TodoController {
private final TodoService todoService; private final TodoService todoService;

View File

@@ -10,7 +10,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
@RestController @RestController
@RequestMapping("/api/version") @RequestMapping("/version")
public class VersionController { public class VersionController {
@Value("${APP_VERSION:dev}") @Value("${APP_VERSION:dev}")

View File

@@ -1,7 +1,11 @@
# backend/src/main/resources/application.properties
server.port=8082
server.servlet.context-path=/api
spring.application.name=backend spring.application.name=backend
spring.datasource.url=jdbc:postgresql://localhost:5432/tododb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

View File

@@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.ActiveProfiles;
// Spring Boot 3.4+ replaces @MockBean with @MockitoBean // Spring Boot 3.4+ replaces @MockBean with @MockitoBean
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -25,6 +26,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(TodoController.class) @WebMvcTest(TodoController.class)
@ActiveProfiles("test")
public class TodoControllerTest { public class TodoControllerTest {
@Autowired @Autowired
@@ -52,7 +54,7 @@ public class TodoControllerTest {
List<Todo> todos = Arrays.asList(todo1, todo2); List<Todo> todos = Arrays.asList(todo1, todo2);
when(todoService.getAllTodos()).thenReturn(todos); when(todoService.getAllTodos()).thenReturn(todos);
mockMvc.perform(get("/api/todos")) mockMvc.perform(get("/todos"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].title", is(todo1.getTitle()))) .andExpect(jsonPath("$[0].title", is(todo1.getTitle())))
@@ -63,7 +65,7 @@ public class TodoControllerTest {
void getTodoById_WhenExists_ReturnsTodo() throws Exception { void getTodoById_WhenExists_ReturnsTodo() throws Exception {
when(todoService.getTodoById(1L)).thenReturn(Optional.of(todo1)); when(todoService.getTodoById(1L)).thenReturn(Optional.of(todo1));
mockMvc.perform(get("/api/todos/1")) mockMvc.perform(get("/todos/1"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.title", is(todo1.getTitle()))) .andExpect(jsonPath("$.title", is(todo1.getTitle())))
.andExpect(jsonPath("$.id", is(1))); .andExpect(jsonPath("$.id", is(1)));
@@ -73,7 +75,7 @@ public class TodoControllerTest {
void getTodoById_WhenNotExists_ReturnsNotFound() throws Exception { void getTodoById_WhenNotExists_ReturnsNotFound() throws Exception {
when(todoService.getTodoById(99L)).thenReturn(Optional.empty()); when(todoService.getTodoById(99L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/todos/99")) mockMvc.perform(get("/todos/99"))
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
} }
@@ -85,7 +87,7 @@ public class TodoControllerTest {
when(todoService.createTodo(any(Todo.class))).thenReturn(savedTodo); when(todoService.createTodo(any(Todo.class))).thenReturn(savedTodo);
mockMvc.perform(post("/api/todos") mockMvc.perform(post("/todos")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(newTodo))) .content(objectMapper.writeValueAsString(newTodo)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -101,7 +103,7 @@ public class TodoControllerTest {
when(todoService.updateTodo(eq(1L), any(Todo.class))).thenReturn(updatedTodo); when(todoService.updateTodo(eq(1L), any(Todo.class))).thenReturn(updatedTodo);
mockMvc.perform(put("/api/todos/1") mockMvc.perform(put("/todos/1")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedInfo))) .content(objectMapper.writeValueAsString(updatedInfo)))
.andExpect(status().isOk()) .andExpect(status().isOk())
@@ -115,7 +117,7 @@ public class TodoControllerTest {
when(todoService.updateTodo(eq(99L), any(Todo.class))).thenReturn(null); when(todoService.updateTodo(eq(99L), any(Todo.class))).thenReturn(null);
mockMvc.perform(put("/api/todos/99") mockMvc.perform(put("/todos/99")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updatedInfo))) .content(objectMapper.writeValueAsString(updatedInfo)))
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
@@ -125,7 +127,7 @@ public class TodoControllerTest {
void deleteTodo_WhenExists_ReturnsNoContent() throws Exception { void deleteTodo_WhenExists_ReturnsNoContent() throws Exception {
when(todoService.deleteTodo(1L)).thenReturn(true); when(todoService.deleteTodo(1L)).thenReturn(true);
mockMvc.perform(delete("/api/todos/1")) mockMvc.perform(delete("/todos/1"))
.andExpect(status().isNoContent()); .andExpect(status().isNoContent());
} }
@@ -133,7 +135,9 @@ public class TodoControllerTest {
void deleteTodo_WhenNotExists_ReturnsNotFound() throws Exception { void deleteTodo_WhenNotExists_ReturnsNotFound() throws Exception {
when(todoService.deleteTodo(99L)).thenReturn(false); when(todoService.deleteTodo(99L)).thenReturn(false);
mockMvc.perform(delete("/api/todos/99")) mockMvc.perform(delete("/todos/99"))
.andExpect(status().isNotFound()); .andExpect(status().isNotFound());
} }
} }

View File

@@ -4,3 +4,4 @@ spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect

View File

@@ -22,7 +22,7 @@ services:
build: ./backend build: ./backend
container_name: todo-backend container_name: todo-backend
ports: ports:
- "8082:8080" - "8082:8082"
environment: environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/tododb - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/tododb
- SPRING_DATASOURCE_USERNAME=postgres - SPRING_DATASOURCE_USERNAME=postgres
@@ -30,17 +30,21 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8082/api/actuator/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
frontend: frontend:
build: build:
context: ./frontend context: ./frontend
args:
VITE_BASE_URL: "http://localhost:8082/api/todos"
container_name: todo-frontend container_name: todo-frontend
ports: ports:
- "5173:80" - "5173:80"
depends_on: depends_on:
- backend backend:
condition: service_healthy
volumes: volumes:

View File

@@ -6,6 +6,10 @@ RUN npm install
COPY . . COPY . .
ARG VITE_APP_VERSION=dev ARG VITE_APP_VERSION=dev
ENV VITE_APP_VERSION=${VITE_APP_VERSION} ENV VITE_APP_VERSION=${VITE_APP_VERSION}
ARG VITE_BASE_URL=http://localhost:8082/api
ENV VITE_BASE_URL=${VITE_BASE_URL}
RUN npm run build RUN npm run build
# Run stage # Run stage

View File

@@ -2,7 +2,7 @@
// Vite automatically injects the variable defined in .env.* based on the current mode. // Vite automatically injects the variable defined in .env.* based on the current mode.
// `import.meta.env.VITE_BASE_URL` is the single source of truth for the API root. // `import.meta.env.VITE_BASE_URL` is the single source of truth for the API root.
const BASE_URL = import.meta.env.VITE_BASE_URL; const BASE_URL = import.meta.env.VITE_BASE_URL + '/todos';
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// API helpers // API helpers