WA5 · Ejercicios de herramientas, seguridad y despliegue¶
Los ejercicios WA5.1, WA5.2 y WA5.5 son de JavaScript sin DOM: escríbelos en un .js/.mjs y ejecútalos con node; tu salida debe ser idéntica al resultado esperado, obtenido ejecutando la solución modelo con Node de verdad. El WA5.4 se comprueba ejecutando tus pruebas con node --test. Los WA5.3 (package.json) y WA5.6 (flujo de GitHub Actions) son de configuración: no se pueden ejecutar aquí; compáralos con la solución modelo. Los marcados con ⭐ son más difíciles.
Ejercicio WA5.1¶
Escapar HTML. Escribe escapar.
// escapar(texto): convierte & < > " ' en sus entidades (& < > " ') para poder meter el texto en HTML sin riesgo.
// El & debe sustituirse a la vez que los demás (no después, o se duplicaría). Cualquier valor se trata como texto (String(...)).
function escapar(texto) {
// ...
}
Para comprobarlo:
console.log(escapar("<script>alert('xss')</script>"));
console.log(escapar('Tom & "Jerry"'));
console.log(escapar(42), escapar(null));
console.log(escapar("<"));
Resultado esperado (ejecutado con Node):
<script>alert('xss')</script>
Tom & "Jerry"
42 null
&lt;
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
function escapar(texto) {
return String(texto).replace(/[&<>"']/g, c =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
console.log(escapar("<script>alert('xss')</script>"));
// ... (resto de la solución bloqueado)Ejercicio WA5.2¶
Solo enlaces seguros. Escribe enlaceSeguro con la clase URL.
// enlaceSeguro(texto): devuelve la URL normalizada (url.href) si el protocolo es http:, https: o mailto:,
// y null en cualquier otro caso (javascript:, data:, un texto que no es una URL...). Sin usar expresiones regulares.
function enlaceSeguro(texto) {
// ...
}
Para comprobarlo:
for (const t of ["https://ejemplo.com/a b", "HTTP://EJEMPLO.COM", "mailto:ana@ejemplo.com", "javascript:alert(1)", " javascript:alert(1)", "ftp://ejemplo.com", "/ruta/relativa"]) {
console.log(JSON.stringify(t), "→", enlaceSeguro(t));
}
Resultado esperado (ejecutado con Node):
"https://ejemplo.com/a b" → https://ejemplo.com/a%20b
"HTTP://EJEMPLO.COM" → http://ejemplo.com/
"mailto:ana@ejemplo.com" → mailto:ana@ejemplo.com
"javascript:alert(1)" → null
" javascript:alert(1)" → null
"ftp://ejemplo.com" → null
"/ruta/relativa" → null
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
function enlaceSeguro(texto) {
try {
const url = new URL(texto);
return ["http:", "https:", "mailto:"].includes(url.protocol) ? url.href : null;
} catch {
// ... (resto de la solución bloqueado)Ejercicio WA5.3¶
Un package.json completo. Escribe el package.json de un proyecto llamado calculadora (versión 0.1.0) que:
- use módulos ES (
"type": "module"), - tenga los scripts
start(ejecutanode src/index.js),test(ejecuta las pruebas con el ejecutor de Node),lint(eslint .) yformat(prettier --write .), - tenga una dependencia de ejecución,
dayjsen^1.11.0, y dos de desarrollo,eslinten^9.0.0yprettieren~3.3.0.
Este ejercicio es de configuración: no hay salida que comparar. Compara tu solución con la modelo, y comprueba con npm run que ves los cuatro scripts.
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
{
"name": "calculadora",
"version": "0.1.0",
"type": "module",
"scripts": {
// ... (resto de la solución bloqueado)Ejercicio WA5.4¶
Tus primeras pruebas con node --test. Estas dos funciones están en primos.mjs. Escribe primos.test.mjs con cuatro pruebas (usa node:test y node:assert/strict):
esPrimodevuelvetruepara 2, 13 y 97.esPrimodevuelvefalsepara 0, 1, 15 y para un decimal como 2.5.primerosPrimos(5)es[2, 3, 5, 7, 11](usadeepEqual).primerosPrimos(-1)lanza unRangeError.
// primos.mjs
export function esPrimo(n) {
if (!Number.isInteger(n) || n < 2) return false;
for (let i = 2; i * i <= n; i++) if (n % i === 0) return false;
return true;
}
export function primerosPrimos(cuantos) {
if (cuantos < 0) throw new RangeError("cuantos no puede ser negativo");
const lista = [];
for (let n = 2; lista.length < cuantos; n++) if (esPrimo(n)) lista.push(n);
return lista;
}
Ejecútalas con node --test primos.test.mjs.
Resultado esperado al ejecutar node --test primos.test.mjs:
✔ esPrimo acepta primos
✔ esPrimo rechaza los que no lo son
✔ primerosPrimos(5)
✔ primerosPrimos con un número negativo lanza RangeError
ℹ tests 4
ℹ pass 4
ℹ fail 0
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
import { test } from "node:test";
import assert from "node:assert/strict";
import { esPrimo, primerosPrimos } from "./primos.mjs";
test("esPrimo acepta primos", () => {
for (const n of [2, 13, 97]) assert.equal(esPrimo(n), true);
// ... (resto de la solución bloqueado)Ejercicio WA5.5¶
Cabeceras de CORS. Escribe cabecerasCors: solo los orígenes de la lista reciben permiso, y nunca *.
// cabecerasCors(origen, permitidos): devuelve un OBJETO con las cabeceras de CORS que debe enviar el servidor.
// - Si `origen` está en el array `permitidos`: { "Access-Control-Allow-Origin": origen, "Vary": "Origin" }
// - Si no está (o es undefined): un objeto vacío {} (NUNCA "*", NUNCA el origen sin comprobar)
function cabecerasCors(origen, permitidos) {
// ...
}
Para comprobarlo:
const ok = ["https://mi-web.es", "https://admin.mi-web.es"];
console.log(cabecerasCors("https://mi-web.es", ok));
console.log(cabecerasCors("https://admin.mi-web.es", ok));
console.log(cabecerasCors("https://mi-web.es.malvado.com", ok));
console.log(cabecerasCors(undefined, ok));
Resultado esperado (ejecutado con Node):
{ 'Access-Control-Allow-Origin': 'https://mi-web.es', Vary: 'Origin' }
{
'Access-Control-Allow-Origin': 'https://admin.mi-web.es',
Vary: 'Origin'
}
{}
{}
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
function cabecerasCors(origen, permitidos) {
if (origen !== undefined && permitidos.includes(origen)) {
return { "Access-Control-Allow-Origin": origen, "Vary": "Origin" };
}
return {};
// ... (resto de la solución bloqueado)Ejercicio WA5.6¶
⭐ Publicar solo si pasan las pruebas. Escribe el archivo .github/workflows/web.yml de un proyecto con npm y Vite que, en cada push a main:
- instale las dependencias de forma exacta (con el archivo de bloqueo),
- ejecute las pruebas,
- construya la web (
npm run build, que deja el resultado endist/), - y solo si todo lo anterior ha ido bien, publique
dist/en GitHub Pages.
Es un archivo de configuración: no se puede ejecutar aquí. Compáralo con la solución modelo; comprueba el orden de los pasos y que la publicación sea un trabajo separado que dependa del anterior.
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
name: Publicar web
on:
push:
branches: [main]
permissions:
# ... (resto de la solución bloqueado)