47 Commits

Author SHA1 Message Date
25ba33d43a Merge pull request 'Update jenkinsfile.ci' (#30) from feature/main-UpdateJenkinsFileCi into main
Reviewed-on: #30
2026-02-06 21:39:23 +01:00
896a7413fe Update Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 21:35:56 +01:00
1c572a30a0 Delete build from CD job 2026-02-06 21:34:34 +01:00
e5cd76c0ba Publish frontend tests
All checks were successful
Tests / Declarative: Post Actions passed: 14
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 21:13:24 +01:00
484412b34e Generate junit xml in frontend tests 2026-02-06 21:11:28 +01:00
9409707329 Update ci msgs 2026-02-06 21:08:25 +01:00
aafc74d876 Delete illegal option
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:59:19 +01:00
408411117c Update shell
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:57:55 +01:00
22f19a2ced Update Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:54:14 +01:00
ac2174d365 Actualizar Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:45:24 +01:00
4ef74b38eb Actualizar Jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:34:23 +01:00
214aec2e73 Actualizar Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:31:19 +01:00
f431f1c5dc Actualizar Jenkinsfile.ci
Some checks failed
Tests / Declarative: Post Actions passed: 5
CI-Multi/pipeline/pr-main There was a failure building this commit
2026-02-06 20:27:14 +01:00
0550e4a431 Update jenkinsfile.ci
All checks were successful
Tests / Declarative: Post Actions No test results found
CI-Multi/pipeline/pr-main This commit looks good
2026-02-06 20:22:25 +01:00
2264b76d9c Merge pull request 'feature/frontend-tests' (#26) from feature/frontend-tests into main
Reviewed-on: #26
Reviewed-by: Jose <husbando_enjoyer@openbokeron+gitea.noreply@geeklab.es>
2026-02-05 20:33:15 +01:00
Abdulee
4ab3ca0642 fix(test): reemplazar tests async fallidos por tests sincronos
All checks were successful
OB/TallerCiCd/pipeline/pr-main This commit looks good
CI-Multi/pipeline/pr-main This commit looks good
2026-02-04 13:48:38 +01:00
Abdulee
beeb58c07a fix(test): configurar jest-dom para tests de componente 2026-02-04 13:36:55 +01:00
Abdulee
5d1f518733 fix(test): eliminar test incorrecto de mayusculas 2026-02-04 13:36:49 +01:00
Abdulee
d2f16e65b2 chore: excluir TESTS.md del repositorio 2026-02-04 13:32:39 +01:00
Abdulee
0fe2569131 ci: ejecutar npm test en pipeline frontend 2026-02-04 13:20:59 +01:00
Abdulee
33a629f47d docs(frontend): añadir documentación de tests 2026-02-04 13:20:29 +01:00
Abdulee
e30ff9b178 test(frontend): añadir tests de componente App 2026-02-04 13:19:30 +01:00
Abdulee
79bfdf78c1 test(frontend): añadir tests de API con mocking 2026-02-04 13:18:36 +01:00
Abdulee
e40ca64536 test(frontend): añadir tests unitarios para prettify() 2026-02-04 13:16:28 +01:00
295accd825 Merge pull request 'Update CI job' (#20) from bugfix/main-11-WorkspaceCleanup into main
Reviewed-on: #20
2026-01-17 22:46:39 +01:00
87b846b4ae Add node
All checks were successful
CI-Multi/pipeline/pr-main This commit looks good
2026-01-17 22:09:31 +01:00
330c2a5364 Update CI job 2026-01-17 12:33:49 +01:00
512c1cea7b Merge pull request 'bugfix/main-16-PreventException' (#18) from bugfix/main-16-PreventException into main
Reviewed-on: #18
2025-12-28 11:35:40 +01:00
5845fed88f Ruff fix 2025-12-28 11:34:30 +01:00
938fd8170c Return error status 2025-12-28 11:34:01 +01:00
1f0c10b458 Merge pull request 'bugfix/main-13-UpdateConfigurations' (#17) from bugfix/main-13-UpdateConfigurations into main
Reviewed-on: #17
2025-12-28 11:15:36 +01:00
b15630c7ea Delete trigger 2025-12-28 11:05:24 +01:00
bc044a10c9 Change base images 2025-12-28 11:01:55 +01:00
558a3198f4 Revert image rotation 2025-12-24 18:23:35 +01:00
8901941e9f Delete conditions 2025-12-24 17:52:14 +01:00
c6780b53fc Use docker compose instead of docker-compose 2025-12-24 14:00:06 +01:00
d49e756c21 Update intervals 2025-12-24 13:39:10 +01:00
12817a7e82 Rotate images 2025-12-24 13:37:00 +01:00
a9baf6da95 Parametrize configurations¡ 2025-12-24 12:44:34 +01:00
a2f21e1286 Add vite api base 2025-12-23 18:53:36 +01:00
7de0f434c3 docker network 2025-12-23 18:40:11 +01:00
c74cc19d1b Add context path 2025-12-23 18:30:53 +01:00
1191ef9f1f Update context path 2025-12-23 18:02:41 +01:00
26128feb7e Update listening port 2025-12-23 17:46:32 +01:00
48dd10ea05 Prevent creation of pycache directory 2025-12-23 17:34:06 +01:00
93fdce1d6e Change jenkins url 2025-12-23 17:33:48 +01:00
b5ace86a03 Change port 2025-12-23 17:33:30 +01:00
23 changed files with 570 additions and 149 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
BACKEND_TAG=latest
FRONTEND_TAG=latest
VITE_API_BASE=/taller/api
JENKINS_BASE_URL=http://host.docker.internal:8080
JENKINS_JOB_NAME=TallerCiCd
JENKINS_USER=
JENKINS_TOKEN=

View File

@@ -2,54 +2,45 @@ pipeline {
agent none
options {
disableConcurrentBuilds()
timestamps()
}
parameters {
string(
name: 'JENKINS_BASE_URL',
defaultValue: 'https://openbokeron.org/jenkins',
description: 'Base URL del Jenkins objetivo'
)
string(
name: 'JENKINS_JOB_NAME',
defaultValue: 'CD',
description: 'Nombre del job que se consulta en Jenkins'
)
string(
name: 'VITE_API_BASE',
defaultValue: '/taller/api',
description: 'Base path/API para el frontend (build time)'
)
}
environment {
NODE_OPTIONS = '--max_old_space_size=2048'
APP_VERSION = "1.0.${BUILD_NUMBER}"
DOCKER_BUILDKIT = '1'
JENKINS_BASE_URL = "${params.JENKINS_BASE_URL}"
JENKINS_JOB_NAME = "${params.JENKINS_JOB_NAME}"
VITE_API_BASE = "${params.VITE_API_BASE}"
PYTHONDONTWRITEBYTECODE = 1
}
stages {
/* =========================
TESTS
========================= */
stage('Backend: test (main)') {
when {
branch 'main'
}
agent {
docker {
image 'python:3.11-slim'
args '-u root'
}
}
steps {
dir('backend') {
sh '''
set -e
python -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements-dev.txt
pytest
'''
}
}
}
/* =========================
DOCKER BUILD
========================= */
stage('Docker: build images') {
when {
branch 'main'
}
agent any
steps {
@@ -72,13 +63,12 @@ pipeline {
--build-arg GIT_COMMIT=${COMMIT_SHORT} \
--build-arg COMMIT_AUTHOR="${COMMIT_AUTHOR}" \
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \
-t cafeteria-backend:${BUILD_NUMBER} \
-t cafeteria-backend:latest \
-t cafeteria-backend:${APP_VERSION} \
./backend
docker build \
-t cafeteria-frontend:${BUILD_NUMBER} \
-t cafeteria-frontend:latest \
--build-arg VITE_API_BASE=${VITE_API_BASE} \
-t cafeteria-frontend:${APP_VERSION} \
./frontend
'''
}
@@ -89,14 +79,7 @@ pipeline {
========================= */
stage('Deploy (docker compose)') {
when {
branch 'main'
}
agent any
environment {
JENKINS_BASE_URL = 'http://jenkins:8080'
JENKINS_JOB_NAME = 'Espetos'
}
steps {
withCredentials([
usernamePassword(
@@ -108,17 +91,16 @@ pipeline {
sh '''
set -e
echo "Deploying backend ${BUILD_NUMBER}"
echo "Deploying ${APP_VERSION}"
echo "BACKEND_TAG=${BUILD_NUMBER}" > .env
echo "FRONTEND_TAG=${BUILD_NUMBER}" >> .env
docker-compose up -d
BACKEND_TAG=${APP_VERSION} FRONTEND_TAG=${APP_VERSION} docker compose up -d
'''
}
}
}
stage('Cleanup') {
agent any
steps {

View File

@@ -38,19 +38,26 @@ pipeline {
agent {
docker {
image 'python:3.11-slim'
args '-u root'
}
}
environment {
HOME = "${WORKSPACE}"
PIP_CACHE_DIR = "${WORKSPACE}/.cache/pip"
PYTHONDONTWRITEBYTECODE = 1
}
steps {
dir('backend') {
sh '''
set -e
mkdir -p "$PIP_CACHE_DIR" "$WORKSPACE/.cache/pytest"
python -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements-dev.txt
ruff check app tests
pytest
pytest -o cache_dir="$WORKSPACE/.cache/pytest" --junitxml=pytest.xml
'''
}
}
@@ -66,23 +73,95 @@ pipeline {
image 'node:20-slim'
}
}
environment {
HOME = "${WORKSPACE}"
NPM_CONFIG_CACHE = "${WORKSPACE}/.cache/npm"
}
steps {
dir('frontend') {
sh '''
set -e
mkdir -p "$NPM_CONFIG_CACHE"
npm install --no-progress --no-audit --prefer-offline
npm run check
mkdir -p test-results
npm test
npm run build
'''
}
}
}
stage('Cleanup') {
agent any
steps {
cleanWs()
}
post {
always {
script {
node {
junit testResults: 'backend/pytest.xml', allowEmptyResults: true
junit testResults: 'frontend/test-results/junit.xml', allowEmptyResults: true
if (env.CHANGE_ID) {
def giteaBase = 'https://openbokeron.org'
def owner = 'OpenBokeron'
def repo = 'TallerCiCd'
def pr = env.CHANGE_ID
def result = currentBuild.currentResult
def msg = ""
if (result == "SUCCESS") {
msg = """
✅ Todo en orden, camarada.
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
else if (result == "FAILURE") {
msg = """
❌ Alto ahí. ¿Qué clase de crímenes de guerra has cometido en esta PR?
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
else {
msg = """
⚠️ Dudoso…
- URL: ${env.BUILD_URL}
""".stripIndent().trim()
}
def commentsUrl = "${giteaBase}/gitea/api/v1/repos/${owner}/${repo}/issues/${pr}/comments"
withCredentials([usernamePassword(credentialsId: 'jenkins-bot-api',
usernameVariable: 'GITEA_USER',
passwordVariable: 'GITEA_TOKEN')]) {
def body = msg.replace('\\', '\\\\').replace('"','\\"').replace('\n','\\n')
// Avoid interpolation of secret variables (https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#string-interpolation)
withEnv(["GITEA_COMMENTS_URL=${commentsUrl}", "GITEA_BODY=${body}"]) {
sh(label: 'Comentar en PR (Gitea)', script: '''
curl -sS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- \
"$GITEA_COMMENTS_URL" <<JSON
{"body":"$GITEA_BODY"}
JSON
''')
}
}
} else {
echo "No es build de PR (CHANGE_ID vacío); no comento."
}
cleanWs()
}
}
}
}
}

View File

@@ -7,6 +7,16 @@
- Python 3.11+ y `pip`
- Node 18+ y `npm`
## Configuracion de entorno (local/prod)
Variables que usa `docker-compose` y el frontend:
```bash
cp .env.example .env
```
Notas:
- `VITE_API_BASE` por defecto apunta a `/taller/api` y el frontend proxya a la API.
- Para Jenkins local en contenedor: `JENKINS_BASE_URL=http://jenkins:8080`. Se hace necesario que back y jenkins estén en la misma red de Docker si se quiere probar en local.
- Para VPS: `JENKINS_BASE_URL=https://openbokeron.org/jenkins`.
## Backend (FastAPI)
```bash
cd backend
@@ -42,6 +52,7 @@ cd frontend
npm install
npm run dev -- --host --port 5173
```
Abrir `http://localhost:5173/taller/`.
Tests, lint/check:
```bash
cd frontend

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim AS builder
FROM docker.io/library/python:3.11-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -9,7 +9,7 @@ COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps -r requirements.txt -w /build/wheels
FROM python:3.11-slim
FROM docker.io/library/python:3.11-slim
WORKDIR /app
# ---- Build args (desde Jenkins) ----

View File

@@ -1,44 +0,0 @@
{
"builds": [
{
"number": 205,
"status": "success",
"branch": "main",
"commit": "9ac3f91",
"author": "Miau",
"finished_at": "2024-05-04T10:20:00Z",
"duration_seconds": 312
},
{
"number": 204,
"status": "failed",
"branch": "feature/nosetioestoesunmock",
"commit": "75c4ba2",
"author": "Miau",
"finished_at": "2024-05-04T09:50:00Z",
"duration_seconds": 188,
"failed_stage": "tests",
"fun_message": "woops"
},
{
"number": 203,
"status": "failed",
"branch": "main",
"commit": "512ca7e",
"author": "Miau",
"finished_at": "2024-05-04T09:10:00Z",
"duration_seconds": 140,
"failed_stage": "lint",
"fun_message": "Nadie pasa en local el linter"
},
{
"number": 202,
"status": "success",
"branch": "hotfix/tehedichoqueestoesunmock?",
"commit": "c73d8ab",
"author": "Miau",
"finished_at": "2024-05-03T18:30:00Z",
"duration_seconds": 276
}
]
}

View File

@@ -16,8 +16,10 @@
import time
from fastapi import FastAPI
import requests
from fastapi import FastAPI, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.services.builds import build_history
from app.services.menu import build_menu
@@ -76,4 +78,10 @@ def price_for_item(item: str):
@app.get("/builds")
def builds():
return build_history()
try:
return build_history()
except (requests.RequestException, ValueError):
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"builds": [], "error": "jenkins_unavailable"},
)

View File

@@ -15,26 +15,17 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import base64
import json
from pathlib import Path
from typing import Dict, List
import requests
from app.settings import settings
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
def _load_json(filename: str) -> Dict:
path = DATA_DIR / filename
with open(path, encoding="utf-8") as file:
return json.load(file)
def _sort_builds(builds: List[Dict]) -> List[Dict]:
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
def normalize_build(build: Dict) -> Dict:
changes = build.get("changeSets", [])
commits = []
@@ -58,11 +49,16 @@ def normalize_build(build: Dict) -> Dict:
def _auth_header() -> Dict[str, str]:
if not settings.jenkins_user or not settings.jenkins_token:
return {}
token = f"{settings.jenkins_user}:{settings.jenkins_token}"
encoded = base64.b64encode(token.encode()).decode()
return {"Authorization": f"Basic {encoded}"}
def fetch_builds(limit: int = 5) -> List[Dict]:
if not settings.jenkins_job_name:
raise ValueError("JENKINS_JOB_NAME not configured")
url = (
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
"?tree=builds[number,url,result,timestamp,duration,"
@@ -78,6 +74,4 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
def build_history() -> Dict:
"""Return Jenkins build history data."""
builds = fetch_builds()
return {
"builds": [normalize_build(b) for b in builds]
}
return {"builds": [normalize_build(b) for b in builds]}

View File

@@ -24,7 +24,10 @@ class RuntimeConfig:
git_commit: str = os.getenv("GIT_COMMIT", "local")
build_number: str = os.getenv("BUILD_NUMBER", "-")
commit_author: str = os.getenv("COMMIT_AUTHOR", "local")
jenkins_base_url: str = os.getenv("JENKINS_BASE_URL", "localhost:8080")
jenkins_base_url: str = os.getenv(
"JENKINS_BASE_URL",
"http://localhost:8080"
).rstrip("/")
jenkins_job_name: str = os.getenv("JENKINS_JOB_NAME", "")
jenkins_user: str = os.getenv("JENKINS_USER", "")
jenkins_token: str = os.getenv("JENKINS_TOKEN", "")

View File

@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import requests
from fastapi.testclient import TestClient
from app.main import app
@@ -106,3 +107,16 @@ def test_build_history(monkeypatch):
assert second["status"] == "running"
assert second["duration_seconds"] == 1
assert second["commits"] == []
def test_build_history_error_returns_empty(monkeypatch):
def raise_error(limit=5):
raise requests.RequestException("boom")
monkeypatch.setattr("app.services.builds.fetch_builds", raise_error)
response = client.get("/builds")
assert response.status_code == 503
body = response.json()
assert body["builds"] == []
assert body["error"] == "jenkins_unavailable"

View File

@@ -2,14 +2,16 @@ services:
backend:
build:
context: ./backend
image: cafeteria-backend:${BACKEND_TAG}
image: cafeteria-backend:${BACKEND_TAG:-latest}
networks:
- cafeteria
ports:
- "8000:8000"
environment:
JENKINS_BASE_URL: ${JENKINS_BASE_URL}
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME}
JENKINS_USER: ${JENKINS_USER}
JENKINS_TOKEN: ${JENKINS_TOKEN}
JENKINS_BASE_URL: ${JENKINS_BASE_URL:-http://jenkins:8080}
JENKINS_JOB_NAME: ${JENKINS_JOB_NAME:-TallerCiCd}
JENKINS_USER: ${JENKINS_USER:-}
JENKINS_TOKEN: ${JENKINS_TOKEN:-}
restart: unless-stopped
healthcheck:
test:
@@ -26,10 +28,21 @@ services:
frontend:
build:
context: ./frontend
image: cafeteria-frontend:${FRONTEND_TAG}
args:
VITE_API_BASE: ${VITE_API_BASE:-/taller/api}
image: cafeteria-frontend:${FRONTEND_TAG:-latest}
networks:
- cafeteria
ports:
- "80:80"
- "8081:8081"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
networks:
cafeteria:
enable_ipv6: true
ipam:
config:
- subnet: 2001:db8::/64

1
frontend/.gitignore vendored
View File

@@ -6,3 +6,4 @@ dist
*.log
.svelte-kit
vite.config.ts.timestamp-*
TESTS.md

View File

@@ -1,12 +1,14 @@
FROM node:20-slim AS build
FROM docker.io/library/node:20-slim AS build
WORKDIR /app
COPY package*.json ./
ARG VITE_API_BASE
ENV VITE_API_BASE=$VITE_API_BASE
RUN npm install --no-progress
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS final
FROM docker.io/library/nginx:1.27-alpine AS final
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
COPY --from=build /app/dist /usr/share/nginx/html/taller
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,11 +1,24 @@
server {
listen 80;
listen 8081;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
location = /taller {
return 301 /taller/;
}
location /taller/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /taller/ {
try_files $uri $uri/ /taller/index.html;
}
}

View File

@@ -8,7 +8,7 @@
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check",
"test": "vitest"
"test": "vitest run --reporter=default --reporter=junit --outputFile=./test-results/junit.xml"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",

View File

@@ -96,8 +96,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (!ENABLE_POLLING) return;
const pricesInterval = setInterval(fetchPrices, 8000);
const ciInterval = setInterval(fetchCiStatus, 10000);
const pricesInterval = setInterval(fetchPrices, 180000);
const ciInterval = setInterval(fetchCiStatus, 300000);
return () => {
clearInterval(pricesInterval);
@@ -467,7 +467,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="openbokeron-logo">
<div class="logo-bubble">
<img
src="/open-bokeron-logo.png"
src={`${import.meta.env.BASE_URL}/open-bokeron-logo.png`}
alt="Logo de Open Bokeron"
loading="lazy"
/>

127
frontend/src/App.test.js Normal file
View File

@@ -0,0 +1,127 @@
/*
CI/CD Workshop
Copyright (C) 2025 OpenBokeron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/svelte';
import App from './App.svelte';
const mockMenu = {
starters: ['Sopa de cocido', 'Ensalada mixta'],
mains: ['Pescado al vapor', 'Pollo asado'],
garnish: ['Patatas fritas'],
desserts: ['Fruta del tiempo', 'Yogur'],
notes: ['Pan y bebida incluidos'],
menu_price: 5.5,
university_deal: {
old_price: 4.5,
current_price: 5.5,
note: 'Gracias al convenio con la Universidad',
},
alternative: { title: 'Plato alternativo', items: [], price: 5.0 },
availability: { last_updated: '12:00' },
espetos_tip: 'Hoy toca sardinas',
};
const mockPrices = {
items: [
{ item: 'cafe', price: 1.2, currency: 'EUR', generated_at: '10:00' },
{ item: 'tostada', price: 2.0, currency: 'EUR', generated_at: '10:00' },
],
};
const mockHealth = {
status: 'ok',
build: 42,
commit: 'abc1234def5678',
author: 'Test Author',
uptime_seconds: 3600,
};
const mockBuilds = {
builds: [
{
number: 42,
status: 'success',
finished_at: Date.now(),
duration_seconds: 60,
commits: [{ commit: 'abc1234', message: 'Fix bug', author: 'Dev' }],
},
],
};
function createMockFetch(url) {
if (url.includes('/menu')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) });
}
if (url.includes('/prices')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) });
}
if (url.includes('/health')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) });
}
if (url.includes('/builds')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) });
}
return Promise.resolve({ ok: false, status: 404 });
}
describe('App.svelte', () => {
beforeEach(() => {
globalThis.fetch = vi.fn(createMockFetch);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renderiza el titulo principal', async () => {
render(App);
await waitFor(() => {
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
});
it('renderiza la seccion hero con eyebrow', async () => {
render(App);
await waitFor(() => {
expect(screen.getByText('Taller CI/CD con Jenkins')).toBeInTheDocument();
});
});
it('renderiza los botones de accion', async () => {
render(App);
await waitFor(() => {
expect(screen.getByText('Refrescar menú')).toBeInTheDocument();
expect(screen.getByText('Recalcular desayunos')).toBeInTheDocument();
});
});
it('renderiza la tarjeta de Open Bokeron', async () => {
render(App);
await waitFor(() => {
expect(screen.getByText('Open Bokeron')).toBeInTheDocument();
expect(
screen.getByText('Somos Open Bokeron, la asociación de software libre de la ETSII.')
).toBeInTheDocument();
});
});
});

View File

@@ -16,5 +16,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const baseUrl = import.meta.env.BASE_URL || '/';
const defaultApiBase = `${baseUrl.replace(/\/$/, '')}/api`;
const rawApiBase = import.meta.env.VITE_API_BASE || defaultApiBase;
export const API_BASE = rawApiBase.replace(/\/$/, '');
export const ENABLE_POLLING = import.meta.env.MODE !== 'test';

View File

@@ -0,0 +1,123 @@
/*
CI/CD Workshop
Copyright (C) 2025 OpenBokeron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getMenu, getPrices, getCiStatus, getBuildHistory } from './api';
describe('API services', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe('getMenu', () => {
it('devuelve datos del menu cuando la API responde correctamente', async () => {
const mockMenu = {
starters: ['Sopa'],
mains: ['Pescado al vapor'],
desserts: ['Fruta'],
};
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockMenu),
});
const result = await getMenu();
expect(fetch).toHaveBeenCalledWith('/taller/api/menu');
expect(result).toEqual(mockMenu);
});
it('lanza error cuando la API falla', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 500,
});
await expect(getMenu()).rejects.toThrow('Respuesta no valida del servidor');
});
});
describe('getPrices', () => {
it('devuelve array de items cuando la API responde', async () => {
const mockResponse = {
items: [
{ item: 'cafe', price: 1.2 },
{ item: 'tostada', price: 2.0 },
],
};
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse),
});
const result = await getPrices();
expect(fetch).toHaveBeenCalledWith('/taller/api/prices');
expect(result).toEqual(mockResponse.items);
});
it('devuelve array vacio si no hay items', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({}),
});
const result = await getPrices();
expect(result).toEqual([]);
});
});
describe('getCiStatus', () => {
it('llama al endpoint /health', async () => {
const mockHealth = { status: 'ok', build: 42 };
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockHealth),
});
const result = await getCiStatus();
expect(fetch).toHaveBeenCalledWith('/taller/api/health');
expect(result).toEqual(mockHealth);
});
});
describe('getBuildHistory', () => {
it('llama al endpoint /builds', async () => {
const mockBuilds = { builds: [{ number: 1 }, { number: 2 }] };
fetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockBuilds),
});
const result = await getBuildHistory();
expect(fetch).toHaveBeenCalledWith('/taller/api/builds');
expect(result).toEqual(mockBuilds);
});
});
});

View File

@@ -0,0 +1,19 @@
/*
CI/CD Workshop
Copyright (C) 2025 OpenBokeron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,38 @@
/*
CI/CD Workshop
Copyright (C) 2025 OpenBokeron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect } from 'vitest';
import { prettify } from './text';
describe('prettify', () => {
it('convierte snake_case a Title Case', () => {
expect(prettify('hello_world')).toBe('Hello World');
});
it('maneja palabras multiples con underscore', () => {
expect(prettify('cafe_con_leche')).toBe('Cafe Con Leche');
});
it('maneja una sola palabra sin underscore', () => {
expect(prettify('bocadillo')).toBe('Bocadillo');
});
it('maneja cadena vacia', () => {
expect(prettify('')).toBe('');
});
});

View File

@@ -1,13 +1,35 @@
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
test: {
environment: 'jsdom',
globals: true,
},
server: {
port: 5173,
},
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
const basePath = '/taller/';
const defaultApiBase = `${basePath.replace(/\/$/, '')}/api`;
const apiBase = (env.VITE_API_BASE || defaultApiBase).replace(/\/$/, '');
const apiProxyPath = apiBase.startsWith('http') ? null : apiBase;
return {
base: basePath,
plugins: [svelte()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/setupTests.js'],
},
server: {
port: 5173,
proxy: apiProxyPath
? {
[apiProxyPath]: {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) =>
path.startsWith(apiProxyPath)
? path.slice(apiProxyPath.length) || '/'
: path,
},
}
: undefined,
},
};
});

View File

@@ -3,10 +3,15 @@ FROM jenkins/jenkins:lts
USER root
RUN apt-get update \
&& apt-get install -y docker.io curl \
&& curl -L https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 \
-o /usr/local/bin/docker-compose \
&& chmod +x /usr/local/bin/docker-compose \
&& apt-get install -y ca-certificates curl gnupg \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& chmod a+r /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y docker-ce-cli docker-compose-plugin \
&& rm -rf /var/lib/apt/lists/*
USER jenkins