apacuana-sdk-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +3 -0
- package/.eslintrc.cjs +22 -0
- package/CHANGELOG.md +1 -0
- package/README.md +291 -0
- package/coverage/clover.xml +171 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/certs.js.html +175 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/api/certs.js.html +295 -0
- package/coverage/lcov-report/src/api/index.html +146 -0
- package/coverage/lcov-report/src/api/revocations.js.html +214 -0
- package/coverage/lcov-report/src/api/signatures.js.html +334 -0
- package/coverage/lcov-report/src/api/users.js.html +307 -0
- package/coverage/lcov-report/src/config/index.html +116 -0
- package/coverage/lcov-report/src/config/index.js.html +175 -0
- package/coverage/lcov-report/src/errors/index.html +116 -0
- package/coverage/lcov-report/src/errors/index.js.html +145 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.js.html +286 -0
- package/coverage/lcov-report/src/utils/constant.js.html +145 -0
- package/coverage/lcov-report/src/utils/helpers.js.html +583 -0
- package/coverage/lcov-report/src/utils/index.html +131 -0
- package/coverage/lcov.info +303 -0
- package/dist/index.js +32927 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +32925 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.cjs +7 -0
- package/package.json +47 -0
- package/rollup.config.js +32 -0
- package/src/api/certs.js +70 -0
- package/src/api/revocations.js +43 -0
- package/src/api/signatures.js +77 -0
- package/src/api/users.js +74 -0
- package/src/auth/index.js +24 -0
- package/src/config/index.js +41 -0
- package/src/errors/index.js +20 -0
- package/src/index.js +67 -0
- package/src/utils/constant.js +20 -0
- package/src/utils/helpers.js +166 -0
- package/src/utils/httpClient.js +187 -0
- package/tests/api/certs.test.js +6 -0
- package/tests/api/revocations.test.js +6 -0
- package/tests/api/signatures.test.js +6 -0
- package/tests/api/users.test.js +115 -0
- package/tests/index.test.js +122 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as pkijs from "pkijs";
|
|
2
|
+
import * as asn1js from "asn1js";
|
|
3
|
+
import { getCrypto } from "pkijs";
|
|
4
|
+
import { STATUS_CERTIFICATE, VERIFICATION_STATUS } from "./constant";
|
|
5
|
+
import { ApacuanaAPIError } from "../errors/index";
|
|
6
|
+
|
|
7
|
+
const getCertificateStatus = (userData, certificateInDevice) => {
|
|
8
|
+
if (!userData) {
|
|
9
|
+
throw new Error("El parámetro userData es requerido.");
|
|
10
|
+
}
|
|
11
|
+
if (typeof certificateInDevice !== "boolean") {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"El parámetro certificateInDevice es requerido y debe ser un valor booleano."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (userData?.verificationstatus?.id !== VERIFICATION_STATUS.APPROVED)
|
|
18
|
+
return STATUS_CERTIFICATE.request;
|
|
19
|
+
if (
|
|
20
|
+
userData?.certificationId &&
|
|
21
|
+
!userData?.cangenerate &&
|
|
22
|
+
!userData?.requestscert?.pending &&
|
|
23
|
+
!certificateInDevice &&
|
|
24
|
+
userData?.certificatestatus !== STATUS_CERTIFICATE.revoque
|
|
25
|
+
)
|
|
26
|
+
return STATUS_CERTIFICATE.certificate_another_device;
|
|
27
|
+
|
|
28
|
+
if (userData?.requestscert?.pending && userData?.certificationId)
|
|
29
|
+
return STATUS_CERTIFICATE.request_revoque;
|
|
30
|
+
if (
|
|
31
|
+
!userData?.cangenerate &&
|
|
32
|
+
userData?.certificatestatus === STATUS_CERTIFICATE.revoque
|
|
33
|
+
)
|
|
34
|
+
return STATUS_CERTIFICATE.revoque;
|
|
35
|
+
|
|
36
|
+
if (!userData?.approvedUser) return STATUS_CERTIFICATE.request;
|
|
37
|
+
if (userData?.cangenerate) return STATUS_CERTIFICATE.generate;
|
|
38
|
+
|
|
39
|
+
if (userData?.certificationId) return STATUS_CERTIFICATE.current;
|
|
40
|
+
|
|
41
|
+
return STATUS_CERTIFICATE.revoque;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const generateKeyPair = async (
|
|
45
|
+
signAlg = "RSASSA-PKCS1-v1_5",
|
|
46
|
+
hashAlg = "SHA-256"
|
|
47
|
+
) => {
|
|
48
|
+
try {
|
|
49
|
+
const crypto = getCrypto();
|
|
50
|
+
if (!crypto) {
|
|
51
|
+
throw new Error("API criptográfica no disponible a través de pkijs.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const keyPair = await crypto.generateKey(
|
|
55
|
+
{
|
|
56
|
+
name: signAlg,
|
|
57
|
+
modulusLength: 2048,
|
|
58
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
59
|
+
hash: { name: hashAlg },
|
|
60
|
+
},
|
|
61
|
+
true,
|
|
62
|
+
["sign", "verify"]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return keyPair;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Error durante la generación del par de claves:", error);
|
|
68
|
+
throw new ApacuanaAPIError(
|
|
69
|
+
`Fallo en la generación del par de claves: ${error.message}`,
|
|
70
|
+
0,
|
|
71
|
+
"KEY_PAIR_GENERATION_ERROR"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const generateCSR = async (keyPair, userData, hashAlg = "SHA-256") => {
|
|
77
|
+
try {
|
|
78
|
+
const { publicKey, privateKey } = keyPair;
|
|
79
|
+
|
|
80
|
+
// 1. Validar los parámetros de entrada.
|
|
81
|
+
if (!keyPair || !publicKey || !privateKey) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"Par de claves no válido. Es necesario un CryptoKeyPair."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!userData || !userData.email) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Datos de usuario incompletos. El correo electrónico es requerido."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pkcs10 = new pkijs.CertificationRequest();
|
|
93
|
+
pkcs10.version = 0;
|
|
94
|
+
|
|
95
|
+
const subjectAttributes = [
|
|
96
|
+
{
|
|
97
|
+
type: "2.5.4.6", // OID para 'Country' (C)
|
|
98
|
+
value: new asn1js.PrintableString({ value: userData.country || "VE" }),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "2.5.4.3", // OID para 'Common Name' (CN)
|
|
102
|
+
value: new asn1js.Utf8String({ value: userData.commonName || "N/A" }),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: "1.2.840.113549.1.9.1", // OID para 'E-mail' (E)
|
|
106
|
+
value: new asn1js.IA5String({ value: userData.email }),
|
|
107
|
+
},
|
|
108
|
+
// Puedes agregar más atributos aquí, como:
|
|
109
|
+
// {
|
|
110
|
+
// type: '2.5.4.8', // OID para 'State or Province' (ST)
|
|
111
|
+
// value: new asn1js.Utf8String({ value: userData.state || 'N/A' }),
|
|
112
|
+
// },
|
|
113
|
+
// {
|
|
114
|
+
// type: '2.5.4.7', // OID para 'Locality' (L)
|
|
115
|
+
// value: new asn1js.Utf8String({ value: userData.locality || 'N/A' }),
|
|
116
|
+
// },
|
|
117
|
+
].filter((attr) => attr.value.value); // Filtra atributos sin valor.
|
|
118
|
+
|
|
119
|
+
// 3. Asignar los atributos al Subject.
|
|
120
|
+
subjectAttributes.forEach((attr) =>
|
|
121
|
+
pkcs10.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue(attr))
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 4. Importar la clave pública y firmar la solicitud con la clave privada.
|
|
125
|
+
await pkcs10.subjectPublicKeyInfo.importKey(publicKey);
|
|
126
|
+
await pkcs10.sign(privateKey, hashAlg);
|
|
127
|
+
|
|
128
|
+
// 5. Codificar el CSR para el envío.
|
|
129
|
+
const csr = pkcs10.toSchema().toBER(false);
|
|
130
|
+
|
|
131
|
+
// 6. Retornar el CSR y el Subject.
|
|
132
|
+
return {
|
|
133
|
+
csr,
|
|
134
|
+
subject: pkcs10.subject,
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Manejo de errores centralizado.
|
|
138
|
+
console.error("Error durante la generación del CSR:", error);
|
|
139
|
+
throw new ApacuanaAPIError(
|
|
140
|
+
`Fallo en la generación del CSR: ${error.message}`,
|
|
141
|
+
0,
|
|
142
|
+
"CSR_GENERATION_ERROR"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Función para convertir ArrayBuffer a Base64
|
|
148
|
+
const arrayBufferToBase64 = (buffer) => {
|
|
149
|
+
const binary = String.fromCharCode.apply(null, new Uint8Array(buffer));
|
|
150
|
+
return window.btoa(binary);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export async function exportPrivateKey(key) {
|
|
154
|
+
const crypto = getCrypto();
|
|
155
|
+
const exported = await crypto.exportKey("pkcs8", key);
|
|
156
|
+
const exportablePrivateKey = new Uint8Array(exported);
|
|
157
|
+
return arrayBufferToBase64(exportablePrivateKey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const helpers = {
|
|
161
|
+
getCertificateStatus,
|
|
162
|
+
generateKeyPair,
|
|
163
|
+
generateCSR,
|
|
164
|
+
exportPrivateKey,
|
|
165
|
+
arrayBufferToBase64,
|
|
166
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/utils/httpClient.js
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { ApacuanaAPIError } from "../errors/index";
|
|
4
|
+
import { getConfig } from "../config/index";
|
|
5
|
+
|
|
6
|
+
let axiosInstance;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Inicializa el cliente HTTP (Axios) para todas las peticiones del SDK.
|
|
10
|
+
* @throws {Error} Si la configuración es incompleta.
|
|
11
|
+
*/
|
|
12
|
+
export const initHttpClient = () => {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
!config.apiUrl ||
|
|
17
|
+
!config.secretKey ||
|
|
18
|
+
!config.apiKey ||
|
|
19
|
+
!config.verificationId ||
|
|
20
|
+
!config.customerId
|
|
21
|
+
) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"HttpClient: Configuración de inicialización incompleta. Llama a apacuana.init() primero."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
axiosInstance = axios.create({
|
|
28
|
+
baseURL: config.apiUrl,
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"APACUANA-API-Key": config.apiKey,
|
|
32
|
+
"APACUANA-SECRET-Key": config.secretKey,
|
|
33
|
+
// La cabecera Authorization se deja vacía al inicio.
|
|
34
|
+
// Se actualizará de forma dinámica con setAuthToken().
|
|
35
|
+
},
|
|
36
|
+
timeout: 30000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Configura los interceptores de Axios
|
|
40
|
+
axiosInstance.interceptors.response.use(
|
|
41
|
+
(response) => {
|
|
42
|
+
// Maneja errores lógicos de la API (si el backend devuelve status 2xx con { success: false })
|
|
43
|
+
if (response.data && response.data.success === false) {
|
|
44
|
+
throw new ApacuanaAPIError(
|
|
45
|
+
response.data.message ||
|
|
46
|
+
"Error lógico en la respuesta de la API (status 2xx)",
|
|
47
|
+
response.status,
|
|
48
|
+
response.data.code || "LOGICAL_API_ERROR"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return response;
|
|
52
|
+
},
|
|
53
|
+
(error) => {
|
|
54
|
+
// Maneja errores HTTP (status 4xx, 5xx) o errores de red
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.error(
|
|
57
|
+
"[HTTP Client] Interceptor de error de Axios:",
|
|
58
|
+
error.response || error.message
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (error.response) {
|
|
62
|
+
const { status, data } = error.response;
|
|
63
|
+
throw new ApacuanaAPIError(
|
|
64
|
+
data.message || `Error en la API: ${status}`,
|
|
65
|
+
status,
|
|
66
|
+
data.code || "API_ERROR"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (error.request) {
|
|
70
|
+
// La petición fue hecha, pero no se recibió respuesta (red caída, CORS, timeout)
|
|
71
|
+
throw new ApacuanaAPIError(
|
|
72
|
+
"Error de conexión a la red, timeout o CORS.",
|
|
73
|
+
0,
|
|
74
|
+
"NETWORK_ERROR"
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
// Algo más ocurrió al configurar la petición
|
|
78
|
+
throw new ApacuanaAPIError(
|
|
79
|
+
`Error desconocido en la petición: ${error.message}`,
|
|
80
|
+
0,
|
|
81
|
+
"UNKNOWN_REQUEST_ERROR"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log(
|
|
89
|
+
"[HTTP Client] Cliente HTTP (Axios) REAL inicializado con URL base:",
|
|
90
|
+
config.apiUrl
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Establece el token de autenticación Bearer para las peticiones subsiguientes.
|
|
96
|
+
* @param {string} token - El token de sesión del usuario.
|
|
97
|
+
* @throws {Error} Si el token es inválido o el cliente no está inicializado.
|
|
98
|
+
*/
|
|
99
|
+
export const setAuthToken = (token) => {
|
|
100
|
+
if (!axiosInstance) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"HttpClient no inicializado. Llama a apacuana.init() primero."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!token || typeof token !== "string") {
|
|
106
|
+
throw new Error("Token de sesión inválido.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Actualizamos la cabecera Authorization de la instancia de Axios.
|
|
110
|
+
axiosInstance.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.log(
|
|
113
|
+
"[HTTP Client] Cabecera Authorization actualizada con nuevo token."
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Envía una petición HTTP a un endpoint de la API.
|
|
119
|
+
* Inyecta automáticamente los parámetros de inicialización en el cuerpo de la petición.
|
|
120
|
+
* @param {string} path - La ruta del endpoint de la API.
|
|
121
|
+
* @param {object} [data={}] - Los datos para enviar en la petición.
|
|
122
|
+
* @param {string} [method='POST'] - El método HTTP.
|
|
123
|
+
* @returns {Promise<object>} Los datos de la respuesta del backend.
|
|
124
|
+
* @throws {ApacuanaAPIError} Si la petición falla.
|
|
125
|
+
*/
|
|
126
|
+
export const httpRequest = async (path, data = {}, method = "POST") => {
|
|
127
|
+
if (!axiosInstance) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"Apacuana SDK: Cliente HTTP no inicializado. " +
|
|
130
|
+
"Llama a apacuana.init() primero."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Creamos el cuerpo de la petición fusionando los datos de la llamada
|
|
135
|
+
// con los parámetros de inicialización.
|
|
136
|
+
const dataToSend = {
|
|
137
|
+
...data,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// eslint-disable-next-line no-console
|
|
141
|
+
console.log(
|
|
142
|
+
`[HTTP Client] Realizando petición ${method} a: ${axiosInstance.defaults.baseURL}${path}`
|
|
143
|
+
);
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(
|
|
146
|
+
"[HTTP Client] Headers (via Axios config):",
|
|
147
|
+
axiosInstance.defaults.headers
|
|
148
|
+
);
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log("[HTTP Client] Datos enviados:", dataToSend);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
let response;
|
|
154
|
+
switch (method.toUpperCase()) {
|
|
155
|
+
case "GET":
|
|
156
|
+
response = await axiosInstance.get(path, { params: dataToSend });
|
|
157
|
+
break;
|
|
158
|
+
case "POST":
|
|
159
|
+
response = await axiosInstance.post(path, dataToSend);
|
|
160
|
+
break;
|
|
161
|
+
case "PUT":
|
|
162
|
+
response = await axiosInstance.put(path, dataToSend);
|
|
163
|
+
break;
|
|
164
|
+
case "DELETE":
|
|
165
|
+
response = await axiosInstance.delete(path, { data: dataToSend });
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
throw new ApacuanaAPIError(
|
|
169
|
+
`Método HTTP no soportado: ${method}`,
|
|
170
|
+
405,
|
|
171
|
+
"UNSUPPORTED_HTTP_METHOD"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Si la promesa se resolvió, la respuesta es exitosa.
|
|
176
|
+
// El interceptor ya manejó los errores 4xx/5xx y los errores lógicos 2xx.
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.log(`[HTTP Client] Respuesta exitosa de ${path}:`, response.data);
|
|
179
|
+
return response.data;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Si la petición falla, el interceptor ya procesó el error.
|
|
182
|
+
// Simplemente relanzamos el error para que sea capturado por la función de API.
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.error(`[HTTP Client] Fallo en la petición a ${path}:`, error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// tests/api/users.test.js
|
|
2
|
+
import { httpRequest } from "../../src/utils/httpClient.js";
|
|
3
|
+
import { getConfig } from "../../src/config/index.js";
|
|
4
|
+
import { ApacuanaAPIError } from "../../src/errors/index.js";
|
|
5
|
+
import getCustomer from "../../src/api/users.js";
|
|
6
|
+
|
|
7
|
+
// Mockear las dependencias:
|
|
8
|
+
jest.mock("../../src/utils/httpClient.js", () => ({
|
|
9
|
+
httpRequest: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock("../../src/config/index.js", () => ({
|
|
13
|
+
getConfig: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("getCustomer", () => {
|
|
17
|
+
const mockConfig = {
|
|
18
|
+
verificationId: "mock-verify-id",
|
|
19
|
+
customerId: "mock-customer-id",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
httpRequest.mockClear();
|
|
24
|
+
getConfig.mockClear();
|
|
25
|
+
getConfig.mockReturnValue(mockConfig);
|
|
26
|
+
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
27
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
console.log.mockRestore();
|
|
32
|
+
console.error.mockRestore();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("debe llamar a httpRequest con los parámetros de la configuración y devolver un objeto con token y userData", async () => {
|
|
36
|
+
const mockResponse = {
|
|
37
|
+
success: true,
|
|
38
|
+
sessionid: "mock-session-id-12345",
|
|
39
|
+
entry: {
|
|
40
|
+
userId: "mock-user",
|
|
41
|
+
name: "John Doe",
|
|
42
|
+
email: "john@example.com",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
httpRequest.mockResolvedValueOnce(mockResponse);
|
|
46
|
+
|
|
47
|
+
const result = await getCustomer();
|
|
48
|
+
|
|
49
|
+
expect(httpRequest).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(httpRequest).toHaveBeenCalledWith(
|
|
51
|
+
"services/api/register/init",
|
|
52
|
+
{
|
|
53
|
+
verificationid: mockConfig.verificationId,
|
|
54
|
+
customerid: mockConfig.customerId,
|
|
55
|
+
},
|
|
56
|
+
"POST"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual({
|
|
60
|
+
token: "mock-session-id-12345",
|
|
61
|
+
userData: {
|
|
62
|
+
userId: "mock-user",
|
|
63
|
+
name: "John Doe",
|
|
64
|
+
email: "john@example.com",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("debe lanzar un error si la configuración está incompleta", async () => {
|
|
70
|
+
getConfig.mockReturnValue({
|
|
71
|
+
verificationId: "mock-verify-id",
|
|
72
|
+
customerId: null,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await expect(getCustomer()).rejects.toThrow(
|
|
76
|
+
"El 'body' con 'userId' es un parámetro requerido para getCustomer."
|
|
77
|
+
);
|
|
78
|
+
expect(httpRequest).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("debe lanzar un ApacuanaAPIError si el backend no devuelve sessionid o entry", async () => {
|
|
82
|
+
httpRequest.mockResolvedValueOnce({
|
|
83
|
+
success: true,
|
|
84
|
+
message: "Error en el backend, no se pudo generar el token.",
|
|
85
|
+
entry: { userId: "mock-user" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await expect(getCustomer()).rejects.toThrow(
|
|
89
|
+
"La respuesta de la API no contiene el usuario."
|
|
90
|
+
);
|
|
91
|
+
expect(httpRequest).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("debe relanzar el ApacuanaAPIError si la petición falla en el httpClient", async () => {
|
|
95
|
+
const apiError = new ApacuanaAPIError(
|
|
96
|
+
"Error de autenticación",
|
|
97
|
+
401,
|
|
98
|
+
"AUTH_ERROR"
|
|
99
|
+
);
|
|
100
|
+
httpRequest.mockRejectedValueOnce(apiError);
|
|
101
|
+
|
|
102
|
+
await expect(getCustomer()).rejects.toThrow(apiError);
|
|
103
|
+
expect(httpRequest).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("debe lanzar un ApacuanaAPIError si ocurre un error inesperado", async () => {
|
|
107
|
+
const genericError = new Error("Error de red o desconocido");
|
|
108
|
+
httpRequest.mockRejectedValueOnce(genericError);
|
|
109
|
+
|
|
110
|
+
await expect(getCustomer()).rejects.toThrow(
|
|
111
|
+
"Fallo inesperado al obtener el token: Error de red o desconocido"
|
|
112
|
+
);
|
|
113
|
+
expect(httpRequest).toHaveBeenCalledTimes(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// tests/index.test.js
|
|
2
|
+
import apacuana from "../src/index.js";
|
|
3
|
+
import { setConfig, getConfig } from "../src/config/index.js";
|
|
4
|
+
import { initHttpClient } from "../src/utils/httpClient.js";
|
|
5
|
+
import getCustomer from "../src/api/users.js";
|
|
6
|
+
|
|
7
|
+
// Mockear todas las dependencias del index.js
|
|
8
|
+
jest.mock("../src/config/index.js", () => ({
|
|
9
|
+
setConfig: jest.fn(),
|
|
10
|
+
getConfig: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("../src/utils/httpClient.js", () => ({
|
|
13
|
+
initHttpClient: jest.fn(),
|
|
14
|
+
setAuthToken: jest.fn(), // Agregar el mock de setAuthToken
|
|
15
|
+
}));
|
|
16
|
+
jest.mock("../src/api/users.js", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe("Apacuana SDK Initialization (init)", () => {
|
|
22
|
+
const mockConfig = {
|
|
23
|
+
apiUrl: "http://test-api.com",
|
|
24
|
+
secretKey: "mock-secret",
|
|
25
|
+
apiKey: "mock-api-key",
|
|
26
|
+
verificationId: "mock-verify-id",
|
|
27
|
+
customerId: "mock-customer-id",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Limpiar todos los mocks antes de cada prueba.
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
|
|
34
|
+
// Configurar el mock de getConfig para devolver una configuración válida por defecto.
|
|
35
|
+
getConfig.mockReturnValue(mockConfig);
|
|
36
|
+
|
|
37
|
+
// Mockear la respuesta exitosa de getCustomer para la mayoría de las pruebas.
|
|
38
|
+
getCustomer.mockResolvedValue({
|
|
39
|
+
token: "mock-session-token",
|
|
40
|
+
userData: { id: "mock-user-id" }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Opcional: Mockear console.log para no ensuciar la consola durante las pruebas.
|
|
44
|
+
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
45
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
// Restaurar los mocks de console después de cada prueba.
|
|
50
|
+
console.log.mockRestore();
|
|
51
|
+
console.error.mockRestore();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("debe llamar a setConfig, initHttpClient y getCustomer en orden y devolver true al finalizar", async () => {
|
|
55
|
+
const result = await apacuana.init(mockConfig);
|
|
56
|
+
|
|
57
|
+
// 1. Verificar que setConfig fue llamado con los parámetros correctos.
|
|
58
|
+
expect(setConfig).toHaveBeenCalledWith(mockConfig);
|
|
59
|
+
|
|
60
|
+
// 2. Verificar que initHttpClient fue llamado sin argumentos.
|
|
61
|
+
expect(initHttpClient).toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
// 3. Verificar que getCustomer fue llamado.
|
|
64
|
+
expect(getCustomer).toHaveBeenCalledWith();
|
|
65
|
+
|
|
66
|
+
// 4. Verificar que se retorna true al finalizar la inicialización.
|
|
67
|
+
expect(result).toBe(true);
|
|
68
|
+
|
|
69
|
+
// 5. Verificar que las funciones se llamaron el número correcto de veces.
|
|
70
|
+
expect(setConfig).toHaveBeenCalledTimes(2); // Una vez inicial, una vez para guardar token
|
|
71
|
+
expect(initHttpClient).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(getCustomer).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("debe lanzar un error si la configuración inicial está incompleta", async () => {
|
|
76
|
+
// Sobrescribir el mock de setConfig para que lance un error.
|
|
77
|
+
setConfig.mockImplementationOnce(() => {
|
|
78
|
+
throw new Error("Apacuana SDK: La configuración inicial debe incluir...");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Verificar que el método init lanza el error esperado.
|
|
82
|
+
await expect(apacuana.init({})).rejects.toThrow(
|
|
83
|
+
"Apacuana SDK: La configuración inicial debe incluir..."
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Verificar que initHttpClient y getCustomer no fueron llamados.
|
|
87
|
+
expect(initHttpClient).not.toHaveBeenCalled();
|
|
88
|
+
expect(getCustomer).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("debe lanzar un error si getCustomer falla", async () => {
|
|
92
|
+
// Configurar el mock de getCustomer para que rechace la promesa.
|
|
93
|
+
getCustomer.mockRejectedValueOnce(
|
|
94
|
+
new Error("Error del servidor al obtener token")
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Verificar que el método init propaga el error de getCustomer.
|
|
98
|
+
await expect(apacuana.init(mockConfig)).rejects.toThrow(
|
|
99
|
+
"Error del servidor al obtener token"
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Verificar que setConfig e initHttpClient sí fueron llamados, pero getCustomer falló.
|
|
103
|
+
expect(setConfig).toHaveBeenCalledWith(mockConfig);
|
|
104
|
+
expect(initHttpClient).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("debe llamar a getConfig y setConfig para guardar el token", async () => {
|
|
108
|
+
const result = await apacuana.init(mockConfig);
|
|
109
|
+
|
|
110
|
+
// La primera llamada a setConfig se hace con la configuración inicial.
|
|
111
|
+
// La segunda llamada se hace para guardar el token y userData.
|
|
112
|
+
expect(setConfig).toHaveBeenCalledTimes(2);
|
|
113
|
+
expect(setConfig).toHaveBeenLastCalledWith({
|
|
114
|
+
...mockConfig,
|
|
115
|
+
token: "mock-session-token",
|
|
116
|
+
userData: { id: "mock-user-id" }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Verificar que el token es devuelto.
|
|
120
|
+
expect(result).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|