1 Commits

Author SHA1 Message Date
e232b78366 Update
All checks were successful
OB/TallerCiCd/pipeline/pr-main This commit looks good
CI-Multi/pipeline/pr-main This commit looks good
2026-01-17 11:44:57 +01:00
10 changed files with 34 additions and 391 deletions

View File

@@ -36,6 +36,32 @@ pipeline {
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
========================= */ ========================= */

View File

@@ -38,12 +38,11 @@ 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
} }
@@ -51,13 +50,12 @@ 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 -o cache_dir="$WORKSPACE/.cache/pytest" --junitxml=pytest.xml pytest
''' '''
} }
} }
@@ -73,95 +71,23 @@ 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
post { steps {
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() cleanWs()
} }
} }
} }
} }
}

View File

@@ -46,5 +46,5 @@ def random_price(item: str) -> Dict:
def prices_payload() -> Dict: def prices_payload() -> Dict:
return { return {
"items": [random_price(item) for item in PRICE_RANGES.keys()], "items": [random_price(item) for item in PRICE_RANGES.keys()],
"disclaimer": "Depende de como pilles al de cafete.", "disclaimer": "Este es un cambio menor a fin de probar Webhooks.",
} }

1
frontend/.gitignore vendored
View File

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

View File

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

View File

@@ -1,127 +0,0 @@
/*
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

@@ -1,123 +0,0 @@
/*
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

@@ -1,19 +0,0 @@
/*
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

@@ -1,38 +0,0 @@
/*
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

@@ -14,7 +14,6 @@ export default defineConfig(({ mode }) => {
test: { test: {
environment: 'jsdom', environment: 'jsdom',
globals: true, globals: true,
setupFiles: ['./src/setupTests.js'],
}, },
server: { server: {
port: 5173, port: 5173,