Compare commits
33 Commits
558a3198f4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ba33d43a | |||
| 896a7413fe | |||
| 1c572a30a0 | |||
| e5cd76c0ba | |||
| 484412b34e | |||
| 9409707329 | |||
| aafc74d876 | |||
| 408411117c | |||
| 22f19a2ced | |||
| ac2174d365 | |||
| 4ef74b38eb | |||
| 214aec2e73 | |||
| f431f1c5dc | |||
| 0550e4a431 | |||
| 2264b76d9c | |||
|
|
4ab3ca0642 | ||
|
|
beeb58c07a | ||
|
|
5d1f518733 | ||
|
|
d2f16e65b2 | ||
|
|
0fe2569131 | ||
|
|
33a629f47d | ||
|
|
e30ff9b178 | ||
|
|
79bfdf78c1 | ||
|
|
e40ca64536 | ||
| 295accd825 | |||
| 87b846b4ae | |||
| 330c2a5364 | |||
| 512c1cea7b | |||
| 5845fed88f | |||
| 938fd8170c | |||
| 1f0c10b458 | |||
| b15630c7ea | |||
| bc044a10c9 |
@@ -34,42 +34,8 @@ pipeline {
|
|||||||
PYTHONDONTWRITEBYTECODE = 1
|
PYTHONDONTWRITEBYTECODE = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
triggers {
|
|
||||||
gitea(
|
|
||||||
branchFilterType: 'Include',
|
|
||||||
branchFilter: 'main',
|
|
||||||
secret: ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
|
|
||||||
/* =========================
|
|
||||||
TESTS
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
stage('Backend: test (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
|
DOCKER BUILD
|
||||||
========================= */
|
========================= */
|
||||||
|
|||||||
@@ -38,11 +38,12 @@ pipeline {
|
|||||||
agent {
|
agent {
|
||||||
docker {
|
docker {
|
||||||
image 'python:3.11-slim'
|
image 'python:3.11-slim'
|
||||||
args '-u root'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
|
HOME = "${WORKSPACE}"
|
||||||
|
PIP_CACHE_DIR = "${WORKSPACE}/.cache/pip"
|
||||||
PYTHONDONTWRITEBYTECODE = 1
|
PYTHONDONTWRITEBYTECODE = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,12 +51,13 @@ pipeline {
|
|||||||
dir('backend') {
|
dir('backend') {
|
||||||
sh '''
|
sh '''
|
||||||
set -e
|
set -e
|
||||||
|
mkdir -p "$PIP_CACHE_DIR" "$WORKSPACE/.cache/pytest"
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
ruff check app tests
|
ruff check app tests
|
||||||
pytest
|
pytest -o cache_dir="$WORKSPACE/.cache/pytest" --junitxml=pytest.xml
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,23 +73,95 @@ pipeline {
|
|||||||
image 'node:20-slim'
|
image 'node:20-slim'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
environment {
|
||||||
|
HOME = "${WORKSPACE}"
|
||||||
|
NPM_CONFIG_CACHE = "${WORKSPACE}/.cache/npm"
|
||||||
|
}
|
||||||
steps {
|
steps {
|
||||||
dir('frontend') {
|
dir('frontend') {
|
||||||
sh '''
|
sh '''
|
||||||
set -e
|
set -e
|
||||||
|
mkdir -p "$NPM_CONFIG_CACHE"
|
||||||
npm install --no-progress --no-audit --prefer-offline
|
npm install --no-progress --no-audit --prefer-offline
|
||||||
npm run check
|
npm run check
|
||||||
|
mkdir -p test-results
|
||||||
|
npm test
|
||||||
npm run build
|
npm run build
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Cleanup') {
|
}
|
||||||
agent any
|
|
||||||
steps {
|
post {
|
||||||
cleanWs()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim AS builder
|
FROM docker.io/library/python:3.11-slim AS builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
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
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
# ---- Build args (desde Jenkins) ----
|
# ---- Build args (desde Jenkins) ----
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fastapi import FastAPI
|
import requests
|
||||||
|
from fastapi import FastAPI, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from app.services.builds import build_history
|
from app.services.builds import build_history
|
||||||
from app.services.menu import build_menu
|
from app.services.menu import build_menu
|
||||||
@@ -76,4 +78,10 @@ def price_for_item(item: str):
|
|||||||
|
|
||||||
@app.get("/builds")
|
@app.get("/builds")
|
||||||
def 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"},
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,26 +15,17 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.settings import settings
|
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]:
|
def _sort_builds(builds: List[Dict]) -> List[Dict]:
|
||||||
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
|
return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def normalize_build(build: Dict) -> Dict:
|
def normalize_build(build: Dict) -> Dict:
|
||||||
changes = build.get("changeSets", [])
|
changes = build.get("changeSets", [])
|
||||||
commits = []
|
commits = []
|
||||||
@@ -58,11 +49,16 @@ def normalize_build(build: Dict) -> Dict:
|
|||||||
|
|
||||||
|
|
||||||
def _auth_header() -> Dict[str, str]:
|
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}"
|
token = f"{settings.jenkins_user}:{settings.jenkins_token}"
|
||||||
encoded = base64.b64encode(token.encode()).decode()
|
encoded = base64.b64encode(token.encode()).decode()
|
||||||
return {"Authorization": f"Basic {encoded}"}
|
return {"Authorization": f"Basic {encoded}"}
|
||||||
|
|
||||||
|
|
||||||
def fetch_builds(limit: int = 5) -> List[Dict]:
|
def fetch_builds(limit: int = 5) -> List[Dict]:
|
||||||
|
if not settings.jenkins_job_name:
|
||||||
|
raise ValueError("JENKINS_JOB_NAME not configured")
|
||||||
url = (
|
url = (
|
||||||
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
|
f"{settings.jenkins_base_url}/job/{settings.jenkins_job_name}/api/json"
|
||||||
"?tree=builds[number,url,result,timestamp,duration,"
|
"?tree=builds[number,url,result,timestamp,duration,"
|
||||||
@@ -78,6 +74,4 @@ def fetch_builds(limit: int = 5) -> List[Dict]:
|
|||||||
def build_history() -> Dict:
|
def build_history() -> Dict:
|
||||||
"""Return Jenkins build history data."""
|
"""Return Jenkins build history data."""
|
||||||
builds = fetch_builds()
|
builds = fetch_builds()
|
||||||
return {
|
return {"builds": [normalize_build(b) for b in builds]}
|
||||||
"builds": [normalize_build(b) for b in builds]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import requests
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
@@ -106,3 +107,16 @@ def test_build_history(monkeypatch):
|
|||||||
assert second["status"] == "running"
|
assert second["status"] == "running"
|
||||||
assert second["duration_seconds"] == 1
|
assert second["duration_seconds"] == 1
|
||||||
assert second["commits"] == []
|
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"
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -6,3 +6,4 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
TESTS.md
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-slim AS build
|
FROM docker.io/library/node:20-slim AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
ARG VITE_API_BASE
|
ARG VITE_API_BASE
|
||||||
@@ -7,7 +7,7 @@ RUN npm install --no-progress
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
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 nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html/taller
|
COPY --from=build /app/dist /usr/share/nginx/html/taller
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check",
|
"check": "svelte-check",
|
||||||
"test": "vitest"
|
"test": "vitest run --reporter=default --reporter=junit --outputFile=./test-results/junit.xml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||||
|
|||||||
127
frontend/src/App.test.js
Normal file
127
frontend/src/App.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
123
frontend/src/services/api.test.js
Normal file
123
frontend/src/services/api.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
frontend/src/setupTests.js
Normal file
19
frontend/src/setupTests.js
Normal 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';
|
||||||
38
frontend/src/utils/text.test.js
Normal file
38
frontend/src/utils/text.test.js
Normal 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('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
|
setupFiles: ['./src/setupTests.js'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Reference in New Issue
Block a user