Ir al contenido

Proveedores: Interfaz IProvider

Los proveedores (Providers) en Testform son adaptadores que traducen las intenciones de sincronización de pruebas hacia plataformas remotas (GitHub, GitLab, Jira, TestRail o APIs internas).

Cualquier proveedor personalizado debe satisfacer el contrato de interfaz IProvider.


Contrato de la Interfaz IProvider

export interface IProvider {
/** Crea un elemento o caso de prueba remoto */
createIssue(payload: IProviderIssuePayload): Promise<IProviderIssueResult>;
/** Actualiza campos o títulos de un elemento existente */
updateIssue(
issueId: number | string,
payload: Partial<IProviderIssuePayload>
): Promise<IProviderIssueResult>;
/** Cierra o archiva un elemento remoto */
closeIssue(issueId: number | string): Promise<void>;
/** Obtiene los datos remotos de un elemento por su ID o número */
getIssue(issueId: number | string): Promise<IProviderIssueResult | null>;
/** Lista elementos remotos filtrados por etiqueta */
listIssuesByLabel(label: string): Promise<IProviderIssueResult[]>;
/** Resuelve el ID o clave de un hito o ciclo por su título */
getMilestoneByTitle(title: string): Promise<number | string | undefined>;
/** Publica un comentario o traza de ejecución sobre el elemento */
createIssueComment(
issueId: number | string,
body: string
): Promise<{ id: number | string }>;
/** Actualiza un comentario existente */
updateIssueComment(commentId: number | string, body: string): Promise<void>;
/** Lista el historial de comentarios y evidencias de un elemento */
listIssueComments(
issueId: number | string
): Promise<{ id: number | string; body: string }[]>;
/** Asocia el elemento a un tablero o proyecto corporativo (opcional) */
addToProject?(issueId: string | number): Promise<string | undefined>;
/** Modifica campos personalizados de un ítem de proyecto (opcional) */
updateProjectItemFields?(
itemId: string,
customFields: Record<string, string>
): Promise<void>;
/** Consulta campos personalizados de un ítem de proyecto (opcional) */
getProjectItemFields?(
issueId: string | number
): Promise<Record<string, string>>;
}

Payloads y Resultados

export interface IProviderIssuePayload {
title: string;
body: string;
labels: string[];
assignees?: string[];
milestone?: number | string;
state?: string;
}
export interface IProviderIssueResult {
id: number | string;
number: number;
title: string;
body?: string;
state: string;
node_id?: string;
labels?: string[];
assignees?: string[];
milestone?: string;
created_at?: string;
updated_at?: string;
}

Creación de un Proveedor Personalizado

Crea ./providers/jira-provider.js:

./providers/jira-provider.js
class JiraProvider {
/**
* @param {Record<string, any>} options Opciones configuradas en main.yml
*/
constructor(options) {
this.baseUrl = options.host || 'https://mycompany.atlassian.net';
this.projectKey = options.project_key || 'QA';
this.apiToken = process.env[options.token_env || 'JIRA_API_TOKEN'];
}
async createIssue({ title, body, labels }) {
// Llamada HTTP POST a la API REST de Jira
const res = await fetch(`${this.baseUrl}/rest/api/3/issue`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fields: {
project: { key: this.projectKey },
summary: title,
description: body,
labels,
issuetype: { name: 'Test' }
}
})
});
const data = await res.json();
return {
id: data.id,
number: parseInt(data.key.split('-')[1], 10),
title,
state: 'open',
labels
};
}
async getIssue(issueId) {
const res = await fetch(`${this.baseUrl}/rest/api/3/issue/${issueId}`, {
headers: { 'Authorization': `Bearer ${this.apiToken}` }
});
if (!res.ok) return null;
const data = await res.json();
return {
id: data.id,
number: parseInt(data.key.split('-')[1], 10),
title: data.fields.summary,
body: data.fields.description,
state: data.fields.status?.name || 'open',
labels: data.fields.labels || []
};
}
async updateIssue(issueId, payload) {
// Lógica de actualización
return this.getIssue(issueId);
}
async closeIssue(issueId) {
// Transición de estado a Done/Closed
}
async listIssuesByLabel(label) {
// JQL query: project = QA AND labels = ...
return [];
}
async getMilestoneByTitle(title) {
return undefined;
}
async createIssueComment(issueId, body) {
const res = await fetch(`${this.baseUrl}/rest/api/3/issue/${issueId}/comment`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ body })
});
const data = await res.json();
return { id: data.id };
}
async updateIssueComment(commentId, body) {}
async listIssueComments(issueId) {
return [];
}
}
module.exports = JiraProvider;

Registro en main.yml

Asocia el conector en tu archivo main.yml mediante el bloque provider::

main.yml
provider:
custom:
source: "./providers/jira-provider.js"
host: "https://jira.company.com"
project_key: "PAYMENTS"
token_env: "JIRA_TOKEN"

Al ejecutar testform plan y testform apply, Testform despachará las operaciones de sincronización a través de tu clase.