Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.flima.application.dashboard.dtos;

public record DashboardDTO(
Long totalVisitors,
String uptime,
Long unreadMessages
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dev.flima.domain.educations.TypeEducation;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.List;
Expand All @@ -26,7 +27,15 @@ public record EducationDTORequest(
@NotBlank(message = "{period.not_null_or_empty}")
String period,

@NotNull(message = "{education.specialization.not_null}")
@NotBlank(message = "{education.specialization.not_null}")
String specialization,

@NotNull(message = "{education.skills.not_null}")
@NotEmpty(message = "{education.skills.not_null}")
List<String> skills,

@NotNull(message = "{education.architectures.not_null}")
@NotEmpty(message = "{education.architectures.not_null}")
List<String> architectures
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public record ExperienceDTORequest(
@NotNull(message = "{experience.bullets.not_null}")
@NotEmpty(message = "{experience.bullets.not_null}")
List<String>bullets,

@NotNull(message = "{experience.technologies.not_null}")
@NotEmpty(message = "{experience.technologies.not_null}")
List<String> technologies,

@NotNull(message = "{experience.icon.not_null}")
@NotBlank(message = "{experience.icon.not_null}")
String icon
) {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.flima.application.projects.dtos.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import java.util.List;
Expand All @@ -18,6 +19,8 @@ public record ProjectDTORequest(
@NotBlank(message = "{project.description.not_null}")
String description,

@NotNull(message = "{project.technologies.not_null}")
@NotEmpty(message = "{project.technologies.not_null}")
List<String>technologies,
String codeSnippet,
String icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ public record StatDTORequest(
@NotBlank(message = "{stat.status.not_null}")
String status,

@NotNull(message = "{stat.objective.not_null}")
@NotBlank(message = "{stat.objective.not_null}")
String objective
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.flima.infrastructure.dashboard;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.util.UUID;

@Entity(name = "visitor_counts")
public class VisitorCountPanacheEntity {
@Id
public UUID id;
public Long count;

Check warning on line 11 in backend/src/main/java/dev/flima/infrastructure/dashboard/VisitorCountPanacheEntity.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make count a static final constant or non-public and provide accessors if needed.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AkHhOUCYuF9YK-SDg&open=AZ4AkHhOUCYuF9YK-SDg&pullRequest=24

public VisitorCountPanacheEntity() {
this.id = UUID.fromString("00000000-0000-0000-0000-000000000000");
this.count = 0L;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.flima.infrastructure.dashboard;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.UUID;

@Provider
@ApplicationScoped
public class VisitorFilter implements ContainerRequestFilter {

@Inject

Check warning on line 17 in backend/src/main/java/dev/flima/infrastructure/dashboard/VisitorFilter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AkHjVUCYuF9YK-SDh&open=AZ4AkHjVUCYuF9YK-SDh&pullRequest=24
EntityManager em;

private static final UUID VISITOR_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");

@Override
@Transactional
public void filter(ContainerRequestContext requestContext) throws IOException {
try {
String path = requestContext.getUriInfo().getPath();
// Increment only for public portfolio views (contents, projects, experiences, educations, stacks, stats)
// Skip auth, dashboard, users and messages (admin/internal)
if (isPublicPath(path)) {
VisitorCountPanacheEntity entity = em.find(VisitorCountPanacheEntity.class, VISITOR_ID);
if (entity == null) {
entity = new VisitorCountPanacheEntity();
em.persist(entity);
}
entity.count++;
}
} catch (Exception e) {
// Silently fail to not break the main application flow
System.err.println("Failed to increment visitor count: " + e.getMessage());

Check warning on line 39 in backend/src/main/java/dev/flima/infrastructure/dashboard/VisitorFilter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of System.err by a logger.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AkHjVUCYuF9YK-SDi&open=AZ4AkHjVUCYuF9YK-SDi&pullRequest=24
}
}

private boolean isPublicPath(String path) {
return path.contains("contents") ||
path.contains("projects") ||
path.contains("experiences") ||
path.contains("educations") ||
path.contains("stacks") ||
path.contains("stats");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.flima.presentation.rest.dashboard;

import dev.flima.application.dashboard.dtos.DashboardDTO;
import dev.flima.domain.users.Role;
import dev.flima.infrastructure.dashboard.VisitorCountPanacheEntity;
import dev.flima.infrastructure.messages.MessagePanacheEntity;

Check warning on line 6 in backend/src/main/java/dev/flima/presentation/rest/dashboard/DashboardResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'dev.flima.infrastructure.messages.MessagePanacheEntity'.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AkHjgUCYuF9YK-SDk&open=AZ4AkHjgUCYuF9YK-SDk&pullRequest=24
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import java.lang.management.ManagementFactory;
import java.util.UUID;

@Path("/dashboardData")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({Role.Labels.OWNER})
public class DashboardResource {

@Inject

Check warning on line 23 in backend/src/main/java/dev/flima/presentation/rest/dashboard/DashboardResource.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AkHjgUCYuF9YK-SDj&open=AZ4AkHjgUCYuF9YK-SDj&pullRequest=24
EntityManager em;

private static final UUID VISITOR_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");

@GET
public DashboardDTO getDashboardData() {
// 1. Get Visitor Count
VisitorCountPanacheEntity visitor = em.find(VisitorCountPanacheEntity.class, VISITOR_ID);
Long totalVisitors = (visitor != null) ? visitor.count : 0L;

// 2. Get Uptime
long uptimeMillis = ManagementFactory.getRuntimeMXBean().getUptime();
String uptime = formatUptime(uptimeMillis);

// 3. Unread messages count (queried directly)
Long unreadMessages = (Long) em.createQuery("SELECT COUNT(m) FROM messages m WHERE m.statusMessage = 'UNREAD'")
.getSingleResult();

return new DashboardDTO(totalVisitors, uptime, unreadMessages);
}

private String formatUptime(long millis) {
long days = java.util.concurrent.TimeUnit.MILLISECONDS.toDays(millis);
long hours = java.util.concurrent.TimeUnit.MILLISECONDS.toHours(millis) % 24;
long minutes = java.util.concurrent.TimeUnit.MILLISECONDS.toMinutes(millis) % 60;

if (days > 0) return String.format("%dd %dh", days, hours);
if (hours > 0) return String.format("%dh %dm", hours, minutes);
return String.format("%dm", minutes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE visitor_counts (
id UUID PRIMARY KEY,
count BIGINT NOT NULL
);

-- Initialize with the constant UUID used in the application
INSERT INTO visitor_counts (id, count) VALUES ('00000000-0000-0000-0000-000000000000', 0);
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.flima.infrastructure.dashboard;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;

Check warning on line 4 in backend/src/test/java/dev/flima/infrastructure/dashboard/VisitorFilterTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'io.restassured.http.ContentType'.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AoBsBzpQtN5oZvH6u&open=AZ4AoBsBzpQtN5oZvH6u&pullRequest=24
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;

@QuarkusTest
class VisitorFilterTest {

@Inject
EntityManager em;

private static final UUID VISITOR_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");

@Test
@Transactional
@DisplayName("Should increment visitor count when accessing public project endpoint")
void shouldIncrementVisitorCount() {
// 1. Get initial count
VisitorCountPanacheEntity initial = em.find(VisitorCountPanacheEntity.class, VISITOR_ID);
long initialCount = (initial != null) ? initial.count : 0L;

// 2. Call public endpoint
given()
.when()
.get("/api/v1/projects")
.then()
.statusCode(200);

// 3. Verify count incremented
// Note: The filter runs in a separate transaction or same?
// In QuarkusTest, we might need to refresh or check after flush
em.clear(); // Clear cache to see DB changes
VisitorCountPanacheEntity updated = em.find(VisitorCountPanacheEntity.class, VISITOR_ID);
long finalCount = (updated != null) ? updated.count : 0L;

assertTrue(finalCount >= initialCount, "Visitor count should not decrease");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.flima.presentation.rest.dashboard;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

Check warning on line 10 in backend/src/test/java/dev/flima/presentation/rest/dashboard/DashboardResourceTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'org.hamcrest.CoreMatchers.is'.

See more on https://sonarcloud.io/project/issues?id=devflima&issues=AZ4AoBp8zpQtN5oZvH6t&open=AZ4AoBp8zpQtN5oZvH6t&pullRequest=24
import static org.hamcrest.CoreMatchers.notNullValue;

@QuarkusTest
class DashboardResourceTest {

@Test
@DisplayName("Should return 401 when accessing dashboard without authentication")
void shouldReturn401WhenUnauthorized() {
given()
.when()
.get("/api/v1/dashboardData")
.then()
.statusCode(401);
}

@Test
@TestSecurity(user = "admin", roles = {"OWNER"})
@DisplayName("Should return dashboard data when authenticated as OWNER")
void shouldReturnDashboardDataWhenAuthorized() {
given()
.contentType(ContentType.JSON)
.when()
.get("/api/v1/dashboardData")
.then()
.statusCode(200)
.body("totalVisitors", notNullValue())
.body("uptime", notNullValue())
.body("unreadMessages", notNullValue());
}
}
2 changes: 2 additions & 0 deletions backend/src/test/resources/application-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ quarkus.flyway.migrate-at-start=true
quarkus.mailer.mock=true

# JWT Test Config (Uses generated RSA keys)
smallrye.jwt.sign.key.location=classpath:privateKey.pk8
mp.jwt.verify.publickey.location=classpath:publicKey.pem
mp.jwt.verify.issuer=https://flima.dev

# Kafka Resilience Config
Expand Down
4 changes: 2 additions & 2 deletions load-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { check, sleep } from 'k6';

export const options = {
stages: [
{ duration: '30s', target: 20 }, // Sobe para 20 usuários em 30s
{ duration: '1m', target: 50 }, // Sobe para 50 usuários em 1m
{ duration: '30s', target: 100 }, // Sobe para 20 usuários em 30s
{ duration: '1m', target: 200 }, // Sobe para 50 usuários em 1m
{ duration: '30s', target: 0 }, // Desce para 0
],
thresholds: {
Expand Down
Loading