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
1 change: 1 addition & 0 deletions Documentation/DEVOPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ Base URL: `http://<host>:8080` (sin HTTPS por ahora). Lista detallada en
| GET/PUT | `/vote-config` | MASTER | configuración de votaciones (PUT crea propuesta de cambio) |
| GET | `/badges` | público | catálogo de insignias (icon_url solo si hay `icon_key`) |
| GET | `/subjects[/{id}]` | público | catálogo de asignaturas (con caché) |
| GET | `/courses[/{id}]` | público | catálogo de cursos con sus asignaturas (con caché) |
| GET | `/stats` | público | estadísticas (archivos, espacio, descargas…) |
| PUT/DELETE | `/users/me/profile-picture` | autenticado | foto de perfil (WebP + recorte, limpia el objeto S3 anterior) |
| PUT | `/admin/users/{id}/ban` · `/unban` | MASTER | moderación de usuarios |
Expand Down
20 changes: 15 additions & 5 deletions Documentation/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,23 @@ Archivo: [gradox2/src/test/java/com/example/gradox2/SubjectIntegrationTest.java]
- Tras la **evicción de caché**, se re-consulta la BD.
- El detalle también se sirve desde caché en la segunda petición.

### 10) StatsIntegrationTest — 2 casos
### 10) CourseIntegrationTest — 6 casos
Archivo: [gradox2/src/test/java/com/example/gradox2/CourseIntegrationTest.java](../gradox2/src/test/java/com/example/gradox2/CourseIntegrationTest.java)

- `GET /courses` devuelve los cursos ordenados por código, cada uno con su lista de asignaturas (ordenadas por código).
- `GET /courses/{id}` devuelve el detalle del curso con sus asignaturas.
- `GET /courses/{id}` con id inexistente → `404 NOT_FOUND`.
- El catálogo de cursos se sirve desde **caché** en la segunda petición.
- Tras la **evicción de caché**, se re-consulta la BD.
- El detalle también se sirve desde caché en la segunda petición.

### 11) StatsIntegrationTest — 2 casos
Archivo: [gradox2/src/test/java/com/example/gradox2/StatsIntegrationTest.java](../gradox2/src/test/java/com/example/gradox2/StatsIntegrationTest.java)

- `GET /stats` es **público** y devuelve la forma esperada (número de archivos, espacio, descargas…).
- Las descargas y subidas se ven reflejadas en las estadísticas.

### 11) ProfilePictureIntegrationTest — 10 casos
### 12) ProfilePictureIntegrationTest — 10 casos
Archivo: [gradox2/src/test/java/com/example/gradox2/ProfilePictureIntegrationTest.java](../gradox2/src/test/java/com/example/gradox2/ProfilePictureIntegrationTest.java)

- Subir foto persiste la `profile_picture_key` (S3) y devuelve una URL firmada.
Expand All @@ -165,7 +175,7 @@ Archivo: [gradox2/src/test/java/com/example/gradox2/ProfilePictureIntegrationTes
- Borrar la foto limpia la clave y elimina el objeto.
- Subir/borrar foto sin autenticación → `403`.

### 12) ForumThreadIntegrationTest — 20 casos
### 13) ForumThreadIntegrationTest — 20 casos
Archivo: [gradox2/src/test/java/com/example/gradox2/ForumThreadIntegrationTest.java](../gradox2/src/test/java/com/example/gradox2/ForumThreadIntegrationTest.java)

#### Comentarios y threads (hilo implícito por archivo)
Expand Down Expand Up @@ -200,14 +210,14 @@ Archivo: [gradox2/src/test/java/com/example/gradox2/ForumThreadIntegrationTest.j
#### Gobernanza
- La eliminación de un archivo aprobada por votación elimina thread y comentarios.

### 13) GlobalJourneyIntegrationTest — 1 caso
### 14) GlobalJourneyIntegrationTest — 1 caso
Archivo: [gradox2/src/test/java/com/example/gradox2/GlobalJourneyIntegrationTest.java](../gradox2/src/test/java/com/example/gradox2/GlobalJourneyIntegrationTest.java)

- `fullUserJourneyFromRegistrationToLogout`
- Un único test E2E que recorre el ciclo completo de un usuario: registro →
verificación → login → operaciones → refresh → logout.

### 14) EmailServiceAsyncTest — 2 casos
### 15) EmailServiceAsyncTest — 2 casos
Archivo: [gradox2/src/test/java/com/example/gradox2/EmailServiceAsyncTest.java](../gradox2/src/test/java/com/example/gradox2/EmailServiceAsyncTest.java)

- `sendEmailRunsOnBackgroundThread`
Expand Down
2 changes: 1 addition & 1 deletion Documentation/openapi.json

Large diffs are not rendered by default.

127 changes: 40 additions & 87 deletions gradox2/src/main/java/com/example/gradox2/init/DataLoader.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.example.gradox2.init;
import com.example.gradox2.persistence.entities.Course;
import com.example.gradox2.persistence.entities.File;
import com.example.gradox2.persistence.entities.GlobalConfig;
import com.example.gradox2.persistence.entities.Subject;
import com.example.gradox2.persistence.entities.User;
import com.example.gradox2.persistence.entities.enums.FileType;
import com.example.gradox2.persistence.entities.enums.FileVisibility;
import com.example.gradox2.persistence.entities.enums.UserRole;
import com.example.gradox2.persistence.repository.CourseRepository;
import com.example.gradox2.persistence.repository.FileRepository;
import com.example.gradox2.persistence.repository.SubjectRepository;
import com.example.gradox2.persistence.repository.UserRepository;
Expand All @@ -33,7 +31,6 @@ public class DataLoader {
@Bean
CommandLineRunner initDatabase(UserRepository userRepository,
SubjectRepository subjectRepository,
CourseRepository courseRepository,
VoteConfigRepository voteConfigRepository,
FileRepository fileRepository,
S3StorageService s3StorageService) {
Expand Down Expand Up @@ -308,91 +305,47 @@ CommandLineRunner initDatabase(UserRepository userRepository,
.enabled(true)
.build());

Course primero = courseRepository.save(Course.builder()
.name("Primer curso")
.code("1")
.build());

Course segundo = courseRepository.save(Course.builder()
.name("Segundo curso")
.code("2")
.build());

java.util.List<Subject> subjects = subjectRepository.saveAll(
java.util.List.of(
// Subjects for "Primer curso"
com.example.gradox2.persistence.entities.Subject.builder()
.name("Matemáticas I")
.code("MAT1")
.course(primero)
.build(),
com.example.gradox2.persistence.entities.Subject.builder()
.name("Lengua Castellana I")
.code("LEN1")
.course(primero)
.build(),
com.example.gradox2.persistence.entities.Subject.builder()
.name("Ciencias Naturales I")
.code("CNA1")
.course(primero)
.build(),
// Subjects for "Segundo curso"
com.example.gradox2.persistence.entities.Subject.builder()
.name("Matemáticas II")
.code("MAT2")
.course(segundo)
.build(),
com.example.gradox2.persistence.entities.Subject.builder()
.name("Lengua Castellana II")
.code("LEN2")
.course(segundo)
.build(),
com.example.gradox2.persistence.entities.Subject.builder()
.name("Ciencias Sociales II")
.code("CSO2")
.course(segundo)
.build()
));

System.out.println("✅ Cursos de prueba insertados en la base de datos.");

// El catálogo de cursos y asignaturas lo siembra la migración Flyway V8.
User demoUploader = userRepository.findByUsername("juan123").orElseThrow();
Subject subject1 = subjects.get(0);

fileRepository.save(File.builder()
.title("Apuntes Algebra PUBLIC")
.description("Visibilidad pública")
.type(FileType.APUNTES)
.objectKey(s3StorageService.put("contenido algebra".getBytes()))
.fileHash("hash1")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.PUBLIC)
.build());

fileRepository.save(File.builder()
.title("Ejercicio Lengua RESTRICTED")
.description("Solo visible para USER y MASTER")
.type(FileType.EJERCICIO)
.objectKey(s3StorageService.put("contenido lengua".getBytes()))
.fileHash("hash2")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.RESTRICTED)
.build());

fileRepository.save(File.builder()
.title("Examen Privado PRIVATE")
.description("Solo visible para MASTER")
.type(FileType.EXAMEN)
.objectKey(s3StorageService.put("contenido examen".getBytes()))
.fileHash("hash3")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.PRIVATE)
.build());

System.out.println("✅ Archivos demo insertados con distintos niveles de visibilidad.");
Subject subject1 = subjectRepository.findByCode("FMAT1").orElse(null);
if (subject1 == null) {
System.out.println("ℹ️ Catálogo de asignaturas no disponible; se omite el seed de archivos demo.");
} else {
fileRepository.save(File.builder()
.title("Apuntes Algebra PUBLIC")
.description("Visibilidad pública")
.type(FileType.APUNTES)
.objectKey(s3StorageService.put("contenido algebra".getBytes()))
.fileHash("hash1")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.PUBLIC)
.build());

fileRepository.save(File.builder()
.title("Ejercicio Lengua RESTRICTED")
.description("Solo visible para USER y MASTER")
.type(FileType.EJERCICIO)
.objectKey(s3StorageService.put("contenido lengua".getBytes()))
.fileHash("hash2")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.RESTRICTED)
.build());

fileRepository.save(File.builder()
.title("Examen Privado PRIVATE")
.description("Solo visible para MASTER")
.type(FileType.EXAMEN)
.objectKey(s3StorageService.put("contenido examen".getBytes()))
.fileHash("hash3")
.subject(subject1)
.uploader(demoUploader)
.visibilityLevel(FileVisibility.PRIVATE)
.build());

System.out.println("✅ Archivos demo insertados con distintos niveles de visibilidad.");
}
} else if (!demoSeedsEnabled) {
System.out.println("ℹ️ Seed de desarrollo desactivado por defecto. Usa ENABLE_DEMO_SEEDS=true para cargar datos demo.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
package com.example.gradox2.persistence.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.example.gradox2.persistence.entities.Course;

public interface CourseRepository extends JpaRepository<Course, Long> {

}
@Query("select distinct c from Course c left join fetch c.subjects s order by c.code asc, s.code asc")
List<Course> findAllWithSubjectsByOrderByCodeAsc();

@Query("select distinct c from Course c left join fetch c.subjects s where c.id = :id")
Optional<Course> findByIdWithSubjects(@Param("id") Long id);

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.example.gradox2.persistence.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.example.gradox2.persistence.entities.Subject;

public interface SubjectRepository extends JpaRepository<Subject, Long> {

java.util.List<Subject> findAllByOrderByCourseIdAscCodeAsc();
@Query("select s from Subject s join fetch s.course c order by c.code asc, s.code asc")
List<Subject> findAllByOrderByCourseCodeAscCodeAsc();

Optional<Subject> findByCode(String code);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.gradox2.presentation.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.gradox2.presentation.dto.course.CourseResponse;
import com.example.gradox2.service.interfaces.ICourseService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController
@RequestMapping("/courses")
@Tag(name = "Asignaturas", description = "Catálogo de asignaturas y cursos para crear propuestas de archivo")
public class CourseController {

private final ICourseService courseService;

public CourseController(ICourseService courseService) {
this.courseService = courseService;
}

@GetMapping
@Operation(summary = "Listar cursos", description = "Devuelve todos los cursos ordenados por código con sus asignaturas")
@ApiResponse(responseCode = "200", description = "Lista de cursos devuelta")
public ResponseEntity<List<CourseResponse>> getAllCourses() {
return ResponseEntity.ok(courseService.listAll());
}

@GetMapping("/{id}")
@Operation(summary = "Ver curso", description = "Devuelve el detalle de un curso con sus asignaturas")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Curso devuelto"),
@ApiResponse(responseCode = "404", description = "Curso no encontrado")
})
public ResponseEntity<CourseResponse> getCourse(@PathVariable Long id) {
return ResponseEntity.ok(courseService.getById(id));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.gradox2.presentation.dto.course;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Schema(description = "Respuesta con los datos de un curso y sus asignaturas")
public class CourseResponse {
@Schema(description = "ID del curso", example = "1")
private Long id;

@Schema(description = "Código del curso", example = "1")
private String code;

@Schema(description = "Nombre del curso", example = "Primer curso")
private String name;

@Schema(description = "Asignaturas del curso")
private List<CourseSubjectResponse> subjects;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.gradox2.presentation.dto.course;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Schema(description = "Asignatura resumida dentro de un curso")
public class CourseSubjectResponse {
@Schema(description = "ID de la asignatura", example = "1")
private Long id;

@Schema(description = "Código de la asignatura", example = "FMAT1")
private String code;

@Schema(description = "Nombre de la asignatura", example = "Fundamentos de Matemáticas")
private String name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/admin/**").hasRole("MASTER") // Sin implementar aún
.requestMatchers(HttpMethod.GET, "/files/**").permitAll()
.requestMatchers(HttpMethod.GET, "/subjects/**").permitAll()
.requestMatchers(HttpMethod.GET, "/courses/**").permitAll()
.requestMatchers(HttpMethod.GET, "/badges/**").permitAll()
.requestMatchers(HttpMethod.GET, "/stats/**").permitAll()
.requestMatchers("/api/auth/**").permitAll()
Expand Down
Loading
Loading