{
 "subtemas": [],
 "tarjetas": [
  {
   "sub": "Seguridad",
   "q": "Arquitectura de Spring Security: la cadena de filtros",
   "a": "SecurityFilterChain: una secuencia de filtros servlet antes de tu controller — SecurityContextPersistenceFilter (carga sesión/token), autenticación (UsernamePassword, BearerToken...), ExceptionTranslationFilter (401/403), FilterSecurityInterceptor/AuthorizationFilter (permisos).\nEl resultado: SecurityContext con Authentication disponible en cualquier punto.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "Config moderna: SecurityFilterChain bean (Spring Security 6)",
   "a": "@Bean SecurityFilterChain chain(HttpSecurity http) { http.csrf(...).authorizeHttpRequests(a -> a.requestMatchers(\"/public/**\").permitAll().anyRequest().authenticated()).oauth2ResourceServer(...).sessionManagement(s -> s.sessionCreationPolicy(STATELESS)); return http.build(); }\nWebSecurityConfigurerAdapter está DEPRECADO — todo con beans.",
   "nivel": "avanzado",
   "clave": true
  },
  {
   "sub": "Seguridad",
   "q": "Authentication vs Authorization (con vocabulario Spring)",
   "a": "Authentication: ¿quién eres? — AuthenticationManager → ProviderManager → AuthenticationProviders (uno valida usuario/pass, otro JWT...).\nAuthorization: ¿qué puedes? — AuthorizationManager contra GrantedAuthorities.\nEn el contexto: SecurityContextHolder.getContext().getAuthentication() (principal, authorities, credentials).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "UserDetailsService y UserDetails: implementación típica",
   "a": "UserDetailsService.loadUserByUsername(String) → UserDetails (username, password hash, authorities, enabled).\nImpl con tu repo de usuarios; Spring valida password con PasswordEncoder.matches.\nSi usas JWT/OAuth sin usuarios internos, quizá no necesites UserDetailsService (custom provider o JWT decoder).",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "PasswordEncoder: BCrypt y mejores prácticas",
   "a": "SIEMPRE almacenar hashes, jamás texto plano.\nDelegatingPasswordEncoder: PasswordEncoderFactories.createDelegatingPasswordEncoder() — soporta {bcrypt}..., migración de formatos.\nBCrypt incluye salt y factor de trabajo (coste 10 default; 10-12 típico).\nNunca MD5/SHA-1 solos para passwords.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Seguridad",
   "q": "Roles vs Authorities: anotaciones y expresiones",
   "a": "Authority = permiso fino (\"pedido:leer\"); ROLE_ es una convención de prefijo (ROLE_ADMIN).\nhttp.authorizeHttpRequests: hasAuthority(\"x\"), hasRole(\"ADMIN\") (añade ROLE_ automáticamente).\nMétodo: @PreAuthorize(\"hasRole('ADMIN') or #id == authentication.principal.id\") — SpEL con parámetros (#id).\nHabilitar: @EnableMethodSecurity.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "JWT flow completo en un backend Spring Boot",
   "a": "1) POST /login con credenciales → AuthenticationManager valida → generas JWT (jjwt/nimbus) con claims (sub, roles, exp) firmado (HS256 secreto / RS256 par de claves).\n2) Cliente envía Authorization: Bearer <token>.\n3) Resource server: oauth2ResourceServer().jwt() valida firma y exp → authorities desde claims (JwtAuthenticationConverter).\nStateless: sin sesión en servidor.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Seguridad",
   "q": "HS256 vs RS256 para firmar JWT",
   "a": "HS256: secreto compartido — simple, pero TODO servicio que valida necesita el secreto (riesgo, no se puede verificar 'solo').\nRS256: par privado/público — firmas con clave privada, cualquiera valida con la pública → ideal multi-servicio y OAuth servers.\nRotación: publicar claves en JWKS endpoint (/.well-known/jwks.json) — el resource server las descarga y cachea.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "Validar JWT a mano: qué DEBES chequear",
   "a": "1) Firma contra JWKS/secreto.\n2) exp (expiración) y nbf/iat razonables.\n3) iss (emisor esperado) y aud (audiencia = tu servicio).\n4) Algoritmo esperado (rechazar 'alg: none' o cambios).\nNunca decodificar SIN validar (Base64 NO es seguridad).\nSpring: oauth2ResourceServer().jwt() con issuer-uri o jwk-set-uri hace todo esto.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "Session vs Stateless: sessionCreationPolicy",
   "a": "IF_REQUIRED (default): crea sesión (apps web clásicas).\nSTATELESS: nunca crea ni usa sesión — cada request trae credenciales (JWT). OBLIGATORIO en APIs con tokens.\nNEVER, ALWAYS, INVALIDATE_ON_AUTHENTICATION... para casos mixtos.\nCon STATELESS, CSRF se deshabilita (no hay cookie de sesión que abuse — ver CSRF).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "CSRF: qué es y cuándo aplica en APIs",
   "a": "Ataque: un sitio malicioso hace el navegador enviar requests con TU cookie de sesión (form oculto, fetch same-site). Solo aplica con autenticación por COOKIE.\nDefensa: token CSRF (Spring: CookieCsrfTokenRepository con HttpOnly=false para SPAs) o SameSite=Lax cookies.\nAPIs stateless con Authorization header → http.csrf(AbstractHttpConfigurer::disable) es correcto.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Seguridad",
   "q": "401 vs 403 en Spring Security y cómo customizar la respuesta",
   "a": "401 no autenticado (sin/vencido token) — AuthenticationEntryPoint.\n403 autenticado pero sin permiso — AccessDeniedHandler.\nCustomizar: exceptionHandling(e -> e.authenticationEntryPoint(mi401).accessDeniedHandler(mi403)) — devolver JSON ProblemDetail en APIs (default redirige a /error o login).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "OAuth2 Client: login social/terceros con Spring",
   "a": "spring-boot-starter-oauth2-client + properties del provider (client-id, secret, authorization-grant-type, redirect-uri).\nhttp.oauth2Login() → flujo Authorization Code completo gestionado por Spring.\nCustom: provider propio en spring.security.oauth2.client.registration.\nEl principal queda OAuth2User (attributes del userinfo).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "OAuth2 Resource Server: proteger tu API con tokens de terceros",
   "a": "oauth2ResourceServer().jwt().issuer-uri(https://authserver) — descarga JWKS y valida.\nRoles desde claims: JwtAuthenticationConverter → setJwtGrantedAuthoritiesConverter (mapear claim 'roles' → SimpleGrantedAuthority).\nProps: spring.security.oauth2.resourceserver.jwt.issuer-uri.\nEl auth server (Keycloak, Auth0, Identity Platform) emite; tú solo validas.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "Multi-tenancy ligero: tenant por header",
   "a": "Filter propio al inicio de la chain: extrae X-Tenant-Id → guarda en ThreadLocal/TenantContext.\nDatasource o schema dinámico: AbstractRoutingDataSource decide por tenant.\nValidar SIEMPRE que el tenant coincida con los claims del token (no confiar en el header solo).\nLimpieza del ThreadLocal al final (pools de hilos).",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "¿Cómo probar endpoints protegidos en tests?",
   "a": "@WithMockUser(username, roles={\"ADMIN\"}) sobre el test o método.\n@WithJwt / @WithSecurityContext para JWTs custom.\nMockMvc + SecurityMockMvcRequestPostProcessors.jwt() (de spring-security-test): .with(jwt().authorities(...)).\nCSRF en MockMvc: .with(csrf()) para POST.",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Seguridad",
   "q": "Secrets en Spring Boot: dónde NO ponerlos y dónde sí",
   "a": "NO: application.yml commiteado, código, images, logs.\nSÍ: variables de entorno (12-factor), Secret Manager/Vault (leer al arranque o con Spring Cloud Vault), archivos montados por K8s.\nPlaceholder: ${DB_PASS} desde env.\nRotación: backend con relectura o restart controlado; jamás loguear properties con password (logging.mask).",
   "nivel": "avanzado",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "La pirámide de testing aplicada a Spring Boot",
   "a": "Base (muchos): tests UNITARIOS de lógica pura sin Spring (rápidos, JUnit+Mockito).\nMedio: tests de SLICE — @WebMvcTest, @DataJpaTest, @JsonTest (levantan SOLO esa capa, segundos).\nCima (pocos): @SpringBootTest integración completa (+Testcontainers con BD real).\nAnti-patrón: todo @SpringBootTest → suite de horas.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "@WebMvcTest: qué levanta y qué debes mockear",
   "a": "Levanta SOLO la capa web: controllers, @ControllerAdvice, filtros/converters MVC, MockMvc autoconfigurado.\nNO levanta services/repos → inyectas @MockitoBean (Spring Boot 3.4+, antes @MockBean) MiService.\nUso: validar mappings, status codes, validación de DTOs, manejo de errores, serialización.",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Testing",
   "q": "MockMvc: el recetario de asserts",
   "a": "mockMvc.perform(get(\"/api/pedidos/1\").header(...))\n.andExpect(status().isOk())\n.andExpect(jsonPath(\"$.cliente.nombre\").value(\"Ana\"))\n.andExpect(jsonPath(\"$.items\", hasSize(3)))\n.andDo(print() / document()).\nPOST con body: contentType(APPLICATION_JSON).content(objectMapper.writeValueAsString(dto)).\njsonPath usa expresiones JsonPath ($ = raíz).",
   "nivel": "intermedio",
   "clave": true
  },
  {
   "sub": "Testing",
   "q": "@DataJpaTest y repositorios: qué prueba y qué trae",
   "a": "Levanta JPA + DataSource EM_MEMORIA (H2 por defecto) + repositorios; transacción por test con rollback.\nConfigurar BD real: @AutoConfigureTestDatabase(replace = NONE) + Testcontainers.\nProbar: queries derivadas/@Query críticas, constraints, mapeo de entidades, paginación.\nH2 ≠ tu motor: para SQL nativo complejo usa Testcontainers con el motor real.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "Testcontainers: BD real en Docker para tus tests",
   "a": "@Testcontainers + @Container static PostgreSQLContainer postgres = new PostgreSQLContainer(\"postgres:16\").\n@DynamicPropertySource registra spring.datasource.url/username/password dinámicos.\nBeneficio: el MISMO motor que producción, migraciones Flyway reales ejecutadas.\nCosto: Docker requerido y segundos extra (contenedores estáticos reutilizados lo mitigan).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "@MockitoBean (ex @MockBean) vs Mockito @Mock: diferencia clave",
   "a": "@MockitoBean: Spring reemplaza el bean EN EL CONTEXTO — para tests con Spring que inyectan dependencias.\n@Mock (Mockito): mock puro sin Spring — para tests unitarios con constructor (new Servicio(mock)).\nTrampa: @MockitoBean crea un NUEVO contexto (cache de contextos se rompe si varían) → suite lenta; minimizar combinaciones distintas.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "Mockito: verify, ArgumentCaptor y stubbing útil",
   "a": "when(repo.findById(1L)).thenReturn(Optional.of(entidad)); thenThrow para excepciones.\nverify(repo).save(captor.capture()); captor.getValue() → assert del objeto guardado.\nnever(), times(2), any(), eq() — mezclar matchers requiere TODOS matchers.\nanyMatch: when(x.metodo(anyString())).thenReturn(...).\nComportamiento default: null/0/false (relaxed).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "@SpringBootTest con puerto real: @LocalServerPort y TestRestTemplate",
   "a": "@SpringBootTest(webEnvironment = RANDOM_PORT): levanta el servidor completo en un puerto libre.\n@LocalServerPort int port inyecta el puerto; cliente: TestRestTemplate (auto-configurado) o RestClient apuntando a localhost:port.\nPara pruebas end-to-end del flujo HTTP real (auth incluida) — pocas, críticas.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "Cómo probar código async y schedulers",
   "a": "Async: capturar el CompletableFuture y .get(timeout), o Awaitility: await().atMost(5, SECONDS).untilAsserted(() -> assertThat(...)).\nScheduled: no duermas 5 min — extraer la lógica a un método testeable y llamarlo directo; o @TestPropertySource con cron acelerado.\nEventos: @RecordApplicationEvents + events.stream(...).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "Fixture de datos de prueba: @Sql, builders y fixtures",
   "a": "@Sql(\"/data/test-usuarios.sql\") ejecuta scripts antes del test (@Sql(executionPhase = AFTER_TEST_METHOD) para limpiar).\n@BeforeEach con builders de entidades (ObjectMother/builder pattern) — datos legibles en el propio test.\nBase de datos en memoria por test: @Transactional rollback automático.\nEvitar shared mutable state entre tests (orden aleatorio ayuda: JUnit method order RANDOM).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "AssertJ: los asserts fluidos que reemplazan a JUnit asserts",
   "a": "assertThat(lista).hasSize(3).extracting(\"nombre\").containsExactly(\"a\",\"b\").\nassertThat(excepcion).isInstanceOf(...).hasMessageContaining(...).\nassertThat(dto).usingRecursiveComparison().ignoringFields(\"id\").isEqualTo(otro).\nwithFailMessage, satisfies(v -> ...). Viene incluido en starter-test.",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "¿Cómo medir y actuar sobre cobertura (JaCoCo)?",
   "a": "Plugin jacoco-maven-plugin: report en target/site/jacoco; check con reglas (BUNDLE line ratio 0.7) que fallen el build si bajo.\nRealismo: 100% es ruido; apunta a lógica de negocio alta, config/trivial baja.\nCobertura no prueba decisiones: un test que ejecuta sin asserts cubre igual (mutation testing lo delata — PIT).",
   "nivel": "intermedio",
   "clave": false
  },
  {
   "sub": "Testing",
   "q": "Test de una clase con @Transactional: ¿se prueba la transacción?",
   "a": "El test @Transactional hace rollback → el commit real NO se ejecuta: constraints deferred, triggers ON COMMIT y behaviours de aislamiento no se validan.\nPara validar eso: test SIN @Transactional + limpieza manual, o TestTransaction.commit() explícito dentro del test.\nElije conscientemente.",
   "nivel": "intermedio",
   "clave": false
  }
 ]
}