first commit
This commit is contained in:
123
apps/api/e2e/zz-ui-dev-server.e2e-spec.ts
Normal file
123
apps/api/e2e/zz-ui-dev-server.e2e-spec.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// TEMPORARY (UI work): API on :3001 with fake Pireon users and seeded data. Not part of the repo.
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import {
|
||||
IdentityProvider,
|
||||
InvalidAccessTokenError,
|
||||
InvalidCredentialsError,
|
||||
} from '@task-stream/api/identity/application';
|
||||
import { User, UserFactory } from '@task-stream/api/identity/domain';
|
||||
import { DomainErrorFilter } from '@task-stream/api/shared/presenters';
|
||||
|
||||
process.env['POSTGRES_URL'] ??= 'postgres://taskstream:taskstream@localhost:5432/taskstream';
|
||||
process.env['POSTGRES_SYNCHRONIZE'] = 'true';
|
||||
process.env['MONGO_URL'] = `mongodb://localhost:27017/taskstream_ui_${Date.now()}`;
|
||||
process.env['REDIS_URL'] ??= 'redis://localhost:6379';
|
||||
process.env['EVENT_STREAM_KEY'] = `task-stream:ui:${Date.now()}`;
|
||||
process.env['GRAPHQL_SCHEMA_FILE'] = '';
|
||||
process.env['AUTH_SESSION_TTL_SECONDS'] = '0';
|
||||
|
||||
const { AppModule } = require('../src/app/app.module');
|
||||
|
||||
const users = new UserFactory();
|
||||
const person = (id: string, name: string, email: string) => users.create({ id, name, email, isActive: true });
|
||||
const people: Record<string, User> = {
|
||||
'token-ana': person('101', 'Ana García', 'ana@pireon.dev'),
|
||||
'token-bob': person('102', 'Bob Martín', 'bob@pireon.dev'),
|
||||
'token-qa': person('103', 'Quinn Ruiz', 'quinn@pireon.dev'),
|
||||
'token-dan': person('104', 'Dan Ortega', 'dan@pireon.dev'),
|
||||
};
|
||||
const byId = (id: string) => Object.values(people).find((p) => p.id === id) ?? null;
|
||||
|
||||
const PATH: Record<string, string[]> = {
|
||||
BACKLOG: [], TODO: ['TODO'], IN_PROGRESS: ['TODO', 'IN_PROGRESS'],
|
||||
IN_REVIEW: ['TODO', 'IN_PROGRESS', 'IN_REVIEW'],
|
||||
READY_FOR_QA: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'READY_FOR_QA'],
|
||||
IN_QA: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'READY_FOR_QA', 'IN_QA'],
|
||||
QA_FAILED: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'READY_FOR_QA', 'IN_QA', 'QA_FAILED'],
|
||||
DONE: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'], CANCELLED: ['CANCELLED'],
|
||||
};
|
||||
|
||||
describe('ui dev server', () => {
|
||||
it('serves until stopped', async () => {
|
||||
const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
|
||||
.overrideProvider(IdentityProvider)
|
||||
.useValue({
|
||||
whoAmI: async (token: string) => { const u = people[token]; if (!u) throw new InvalidAccessTokenError(); return u; },
|
||||
findUser: async (id: string) => byId(id),
|
||||
listUsers: async () => Object.values(people),
|
||||
// Email + any password "pireon" → that user's token.
|
||||
issueToken: async (grant: { type: string; username?: string; password?: string }) => {
|
||||
const entry = Object.entries(people).find(([, u]) => u.email === grant.username);
|
||||
if (grant.type !== 'password' || !entry || grant.password !== 'pireon') throw new InvalidCredentialsError('Email o contraseña incorrectos');
|
||||
return { accessToken: entry[0], refreshToken: null, expiresIn: 86400, tokenType: 'Bearer' };
|
||||
},
|
||||
authorizationConfig: () => ({ authorizeUrl: 'https://oauth.pireon.dev/oauth/authorize', clientId: 'ui-dev', scopes: ['users:read'], passwordGrant: true }),
|
||||
})
|
||||
.compile();
|
||||
const app = moduleRef.createNestApplication({ rawBody: true });
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
app.useGlobalFilters(new DomainErrorFilter());
|
||||
await app.listen(3001);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const gql = async (token: string, query: string, variables?: object): Promise<any> => {
|
||||
const r = await fetch('http://localhost:3001/graphql', { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify({ query, variables }) });
|
||||
const body = await r.json();
|
||||
if (body.errors) throw new Error(JSON.stringify(body.errors));
|
||||
return body.data;
|
||||
};
|
||||
const ana = 'token-ana';
|
||||
const project = async (key: string, name: string, description: string) => (await gql(ana, `mutation($i: CreateProjectInput!) { createProject(input: $i) { id } }`, { i: { key, name, description } })).createProject.id as string;
|
||||
const member = (projectId: string, userId: string, role: string) => gql(ana, `mutation($i: ProjectMemberInput!) { addProjectMember(input: $i) { version } }`, { i: { projectId, userId, role } });
|
||||
const release = async (projectId: string, name: string, targetDate: string, description: string) => (await gql(ana, `mutation($i: CreateReleaseInput!) { createRelease(input: $i) { id } }`, { i: { projectId, name, targetDate, description } })).createRelease.id as string;
|
||||
const move = async (issueId: string, status: string, reason?: string) => {
|
||||
for (const step of PATH[status]) await gql(ana, `mutation($i: ChangeIssueStatusInput!) { changeIssueStatus(input: $i) { version } }`, { i: { issueId, status: step, reason: step === 'QA_FAILED' || step === 'CANCELLED' ? reason : undefined } });
|
||||
};
|
||||
const issue = async (projectId: string, i: Record<string, unknown>, status = 'BACKLOG', reason?: string) => {
|
||||
const id = (await gql(ana, `mutation($i: CreateIssueInput!) { createIssue(input: $i) { id } }`, { i: { projectId, ...i } })).createIssue.id as string;
|
||||
await move(id, status, reason);
|
||||
return id;
|
||||
};
|
||||
const comment = (token: string, issueId: string, body: string) => gql(token, `mutation($i: ID!, $b: String!) { addComment(issueId: $i, body: $b) { id } }`, { i: issueId, b: body });
|
||||
const link = (issueId: string, targetIssueId: string, type: string) => gql(ana, `mutation($i: LinkIssuesInput!) { linkIssues(input: $i) { version } }`, { i: { issueId, targetIssueId, type } });
|
||||
|
||||
const web = await project('WEB', 'Checkout revamp', 'Rediseño del checkout: pago en un paso, cupones y QA en cada release.');
|
||||
for (const [id, role] of [['102', 'MEMBER'], ['103', 'MEMBER'], ['104', 'ADMIN']]) await member(web, id, role);
|
||||
const v132 = await release(web, 'v1.3.2', '2026-09-26', 'Tarjetas guardadas y webhooks v2.');
|
||||
const v140 = await release(web, 'v1.4.0', '2026-10-17', 'Checkout en un paso.');
|
||||
await release(web, 'v1.5.0', '2026-11-14', 'Nuevos métodos de pago.');
|
||||
const pay = await issue(web, { type: 'BUG', priority: 'CRITICAL', area: 'FRONTEND', assigneeId: '102', releaseId: v140, labels: ['safari', 'checkout'], title: 'El botón de pagar no responde en Safari 17', description: '1. Abrir el checkout en Safari 17\n2. Rellenar la tarjeta\n3. Pulsar «Pagar»: no pasa nada y no hay error en consola.' }, 'IN_PROGRESS');
|
||||
const cart = await issue(web, { type: 'FEATURE', priority: 'HIGH', area: 'BACKEND', assigneeId: '104', releaseId: v140, labels: ['carrito'], title: 'Guardar el carrito entre dispositivos', description: 'El carrito se asocia al usuario y se fusiona al iniciar sesión.' }, 'IN_REVIEW');
|
||||
const summary = await issue(web, { type: 'UI_CHANGE', priority: 'MEDIUM', area: 'DESIGN', assigneeId: '102', releaseId: v140, figmaUrl: 'https://www.figma.com/design/abc123/Checkout?node-id=12-345', title: 'Nuevo resumen del pedido según Figma' }, 'READY_FOR_QA');
|
||||
const regression = await issue(web, { type: 'QA', priority: 'HIGH', area: 'QA', assigneeId: '103', releaseId: v140, title: 'Regresión del checkout en iOS y Android' }, 'IN_QA');
|
||||
const coupon = await issue(web, { type: 'BUG', priority: 'HIGH', area: 'BACKEND', assigneeId: '104', releaseId: v140, labels: ['cupones'], title: 'El cupón se aplica dos veces si se recarga la página' }, 'QA_FAILED', 'Sigue duplicándose si se recarga durante el pago.');
|
||||
await issue(web, { type: 'TASK', priority: 'LOW', area: 'DEVOPS', assigneeId: '104', title: 'Alertas de latencia del servicio de pagos' }, 'TODO');
|
||||
await issue(web, { type: 'FEATURE', priority: 'MEDIUM', area: 'FRONTEND', title: 'Pago con Bizum' });
|
||||
await issue(web, { type: 'BUG', priority: 'LOW', area: 'FRONTEND', assigneeId: '102', title: 'El foco se pierde al cerrar el modal de dirección' }, 'TODO');
|
||||
await issue(web, { type: 'UI_CHANGE', priority: 'LOW', area: 'DESIGN', title: 'Estados vacíos del historial de pedidos' });
|
||||
await issue(web, { type: 'TASK', priority: 'MEDIUM', area: 'BACKEND', assigneeId: '104', releaseId: v132, title: 'Migrar webhooks de Stripe a la v2' }, 'DONE');
|
||||
await issue(web, { type: 'FEATURE', priority: 'HIGH', area: 'FRONTEND', assigneeId: '102', releaseId: v132, title: 'Guardar tarjeta para próximas compras' }, 'DONE');
|
||||
await issue(web, { type: 'QA', priority: 'MEDIUM', area: 'QA', assigneeId: '103', title: 'Pruebas exploratorias de devoluciones' }, 'TODO');
|
||||
await issue(web, { type: 'BUG', priority: 'MEDIUM', area: 'MOBILE', title: 'La app se cierra al girar en el paso de envío' });
|
||||
await issue(web, { type: 'TASK', priority: 'LOWEST', area: 'DEVOPS', title: 'Limpiar feature flags del checkout antiguo' }, 'CANCELLED', 'Se hará con la limpieza general de flags.');
|
||||
await gql(ana, `mutation($r: ID!) { publishRelease(releaseId: $r) { version } }`, { r: v132 });
|
||||
await link(regression, summary, 'TESTS');
|
||||
await link(summary, cart, 'DEPENDS_ON');
|
||||
await link(coupon, regression, 'BLOCKS');
|
||||
await comment('token-bob', pay, 'Reproducido: el `onClick` llega, pero el iframe de pago no termina de cargar en Safari.');
|
||||
await comment('token-qa', pay, 'En Safari 16 funciona. Adjunto vídeo en el ticket de regresión.');
|
||||
await comment(ana, pay, 'Decisión: usar redirect en lugar de popup para el 3-D Secure.');
|
||||
const api = await project('API', 'Plataforma API', 'Servicios compartidos: identidad, pagos y notificaciones.');
|
||||
await member(api, '104', 'ADMIN');
|
||||
await issue(api, { type: 'FEATURE', priority: 'HIGH', area: 'BACKEND', assigneeId: '104', title: 'Rate limiting por cliente' }, 'IN_PROGRESS');
|
||||
await issue(api, { type: 'BUG', priority: 'MEDIUM', area: 'BACKEND', title: 'Timeout en /payments/refund con importes altos' }, 'TODO');
|
||||
const mob = await project('MOB', 'App móvil', 'iOS y Android: compra rápida y notificaciones.');
|
||||
await member(mob, '102', 'MEMBER');
|
||||
await issue(mob, { type: 'UI_CHANGE', priority: 'MEDIUM', area: 'MOBILE', title: 'Nueva navegación inferior' }, 'IN_REVIEW');
|
||||
|
||||
console.log('UI DEV API READY on :3001');
|
||||
await new Promise(() => undefined);
|
||||
}, 24 * 60 * 60 * 1000);
|
||||
});
|
||||
Reference in New Issue
Block a user