756 lines
21 KiB
JavaScript
756 lines
21 KiB
JavaScript
import { test, describe, mock } from 'node:test';
|
|
import assert from 'node:assert';
|
|
import { KeycloakAPI } from '../../src/utils/keycloak.js';
|
|
import http from 'http';
|
|
import https from 'https';
|
|
import fs from 'fs';
|
|
|
|
describe('KeycloakAPI', () => {
|
|
describe('constructor', () => {
|
|
test('должен создавать экземпляр с HTTP клиентом когда ssl=false', () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
|
|
assert.ok(keycloak);
|
|
assert.strictEqual(keycloak.keycloakData, keycloakData);
|
|
assert.strictEqual(keycloak.client, http);
|
|
assert.strictEqual(keycloak.ca, undefined);
|
|
});
|
|
|
|
test('должен создавать экземпляр с HTTPS клиентом когда ssl=true', () => {
|
|
// Мокаем fs.readFileSync
|
|
const originalReadFileSync = fs.readFileSync;
|
|
let mockReadFileSyncCalls = [];
|
|
const mockReadFileSync = (...args) => {
|
|
mockReadFileSyncCalls.push(args);
|
|
return 'mock-cert-content';
|
|
};
|
|
fs.readFileSync = mockReadFileSync;
|
|
|
|
try {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8443,
|
|
realm: 'test-realm',
|
|
ssl: 'true',
|
|
cert: '/path/to/cert.pem',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
|
|
assert.ok(keycloak);
|
|
assert.strictEqual(keycloak.keycloakData, keycloakData);
|
|
assert.strictEqual(keycloak.client, https);
|
|
assert.strictEqual(keycloak.ca, 'mock-cert-content');
|
|
assert.strictEqual(mockReadFileSyncCalls.length, 1);
|
|
assert.strictEqual(mockReadFileSyncCalls[0][0], '/path/to/cert.pem');
|
|
} finally {
|
|
fs.readFileSync = originalReadFileSync;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getToken', () => {
|
|
test('должен успешно получать токен при валидных credentials', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const mockResponse = {
|
|
access_token: 'test-access-token',
|
|
expires_in: 3600,
|
|
refresh_token: 'test-refresh-token'
|
|
};
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let responseCallback;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
responseCallback = callback;
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
|
|
// Вызываем callback с ответом синхронно
|
|
callback(mockRes);
|
|
|
|
const mockReq = {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
// Симулируем получение данных после end
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from(JSON.stringify(mockResponse)));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
return mockReq;
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'testuser',
|
|
password: 'testpass'
|
|
};
|
|
|
|
const result = await keycloak.getToken(loginData);
|
|
|
|
assert.strictEqual(result.code, 200);
|
|
assert.ok(result.payload);
|
|
assert.strictEqual(result.payload.accessToken, 'test-access-token');
|
|
assert.strictEqual(result.payload.expiresIn, 3600);
|
|
assert.strictEqual(result.payload.refreshToken, 'test-refresh-token');
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
|
|
test('должен возвращать код ошибки при неверных credentials', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Мокаем http.request для ошибки 401
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 401,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from('Unauthorized'));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'wronguser',
|
|
password: 'wrongpass'
|
|
};
|
|
|
|
const result = await keycloak.getToken(loginData);
|
|
|
|
assert.strictEqual(result.code, 401);
|
|
assert.ok(!result.payload);
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
|
|
test('должен обрабатывать ошибки сети', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Мокаем http.request для ошибки сети
|
|
const originalRequest = http.request;
|
|
let errorHandler;
|
|
|
|
http.request = mock.fn(() => {
|
|
const mockReq = {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
// Вызываем обработчик ошибки после end
|
|
if (errorHandler) {
|
|
errorHandler(new Error('Network error'));
|
|
}
|
|
}),
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'error') {
|
|
errorHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
return mockReq;
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'testuser',
|
|
password: 'testpass'
|
|
};
|
|
|
|
await assert.rejects(
|
|
async () => await keycloak.getToken(loginData),
|
|
/Request error: Network error/
|
|
);
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
|
|
test('должен обрабатывать невалидный JSON ответ', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Мокаем http.request для невалидного JSON
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from('invalid json'));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'testuser',
|
|
password: 'testpass'
|
|
};
|
|
|
|
await assert.rejects(
|
|
async () => await keycloak.getToken(loginData),
|
|
/Failed to parse response/
|
|
);
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getAdminToken', () => {
|
|
test('должен успешно получать admin токен', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const mockResponse = {
|
|
access_token: 'admin-access-token',
|
|
expires_in: 3600,
|
|
refresh_token: 'admin-refresh-token'
|
|
};
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
// Проверяем, что используется admin-cli
|
|
assert.ok(options.path.includes('/realms/test-realm'));
|
|
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from(JSON.stringify(mockResponse)));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'admin',
|
|
password: 'adminpass'
|
|
};
|
|
|
|
const result = await keycloak.getAdminToken(loginData);
|
|
|
|
assert.strictEqual(result.accessToken, 'admin-access-token');
|
|
assert.strictEqual(result.expiresIn, 3600);
|
|
assert.strictEqual(result.refreshToken, 'admin-refresh-token');
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
|
|
test('должен отклонять промис при ошибке', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Мокаем http.request для ошибки 401
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 401,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from('Unauthorized'));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const loginData = {
|
|
username: 'wrongadmin',
|
|
password: 'wrongpass'
|
|
};
|
|
|
|
await assert.rejects(
|
|
async () => await keycloak.getAdminToken(loginData),
|
|
/Request failed with status code: 401/
|
|
);
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getUserInfo', () => {
|
|
test('должен успешно получать информацию о пользователе', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const mockUserInfo = {
|
|
sub: 'user-id-123',
|
|
email: 'user@example.com',
|
|
name: 'Test User'
|
|
};
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
assert.strictEqual(options.method, 'GET');
|
|
assert.ok(options.path.includes('/userinfo'));
|
|
assert.strictEqual(options.headers.Authorization, 'Bearer test-token');
|
|
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from(JSON.stringify(mockUserInfo)));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const result = await keycloak.getUserInfo('test-token');
|
|
|
|
assert.ok(result);
|
|
assert.strictEqual(result.sub, 'user-id-123');
|
|
assert.strictEqual(result.email, 'user@example.com');
|
|
assert.strictEqual(result.name, 'Test User');
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
|
|
test('должен обрабатывать ошибку 401', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
const mockRes = {
|
|
statusCode: 401,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from('Unauthorized'));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
await assert.rejects(
|
|
async () => await keycloak.getUserInfo('invalid-token'),
|
|
/Request failed with status code: 401/
|
|
);
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getUserByUsername', () => {
|
|
test('должен успешно получать пользователя по username', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const mockUsers = [
|
|
{
|
|
id: 'user-123',
|
|
username: 'testuser',
|
|
email: 'testuser@example.com'
|
|
}
|
|
];
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
assert.strictEqual(options.method, 'GET');
|
|
assert.ok(options.path.includes('/admin/realms/test-realm/users'));
|
|
assert.ok(options.path.includes('username=testuser'));
|
|
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from(JSON.stringify(mockUsers)));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const result = await keycloak.getUserByUsername('admin-token', 'testuser');
|
|
|
|
assert.ok(result);
|
|
assert.ok(Array.isArray(result));
|
|
assert.strictEqual(result[0].username, 'testuser');
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getUserGroup', () => {
|
|
test('должен успешно получать группы пользователя', async () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const mockGroups = [
|
|
{ id: 'group-1', name: 'Admins' },
|
|
{ id: 'group-2', name: 'Users' }
|
|
];
|
|
|
|
// Мокаем http.request
|
|
const originalRequest = http.request;
|
|
let dataHandler;
|
|
let endHandler;
|
|
|
|
http.request = mock.fn((options, callback) => {
|
|
assert.strictEqual(options.method, 'GET');
|
|
assert.ok(options.path.includes('/admin/realms/test-realm/users/user-123/groups'));
|
|
|
|
const mockRes = {
|
|
statusCode: 200,
|
|
on: mock.fn((event, handler) => {
|
|
if (event === 'data') {
|
|
dataHandler = handler;
|
|
}
|
|
if (event === 'end') {
|
|
endHandler = handler;
|
|
}
|
|
})
|
|
};
|
|
callback(mockRes);
|
|
|
|
return {
|
|
write: mock.fn(),
|
|
end: mock.fn(() => {
|
|
if (dataHandler) {
|
|
dataHandler(Buffer.from(JSON.stringify(mockGroups)));
|
|
}
|
|
if (endHandler) {
|
|
endHandler();
|
|
}
|
|
}),
|
|
on: mock.fn()
|
|
};
|
|
});
|
|
|
|
try {
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const result = await keycloak.getUserGroup('admin-token', 'user-123');
|
|
|
|
assert.ok(result);
|
|
assert.ok(Array.isArray(result));
|
|
assert.strictEqual(result.length, 2);
|
|
assert.strictEqual(result[0].name, 'Admins');
|
|
} finally {
|
|
http.request = originalRequest;
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getDataFromJWT', () => {
|
|
test('должен декодировать JWT токен', () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
// Создаем простой JWT токен (без подписи для теста)
|
|
const payload = {
|
|
sub: 'user-123',
|
|
email: 'test@example.com',
|
|
exp: Math.floor(Date.now() / 1000) + 3600
|
|
};
|
|
|
|
// Простой base64 encoded JWT (header.payload.signature)
|
|
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
const payloadEncoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
const token = `${header}.${payloadEncoded}.signature`;
|
|
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const result = keycloak.getDataFromJWT(token);
|
|
|
|
assert.ok(result);
|
|
assert.ok(result.payload);
|
|
assert.strictEqual(result.payload.sub, 'user-123');
|
|
assert.strictEqual(result.payload.email, 'test@example.com');
|
|
});
|
|
|
|
test('должен возвращать null для невалидного токена', () => {
|
|
const keycloakData = {
|
|
host: 'localhost',
|
|
port: 8080,
|
|
realm: 'test-realm',
|
|
ssl: 'false',
|
|
grant_type: 'password',
|
|
client_id: 'test-client',
|
|
client_secret: 'test-secret'
|
|
};
|
|
|
|
const keycloak = new KeycloakAPI(keycloakData);
|
|
const result = keycloak.getDataFromJWT('invalid.token.here');
|
|
|
|
// jwt.decode может вернуть null для невалидного токена
|
|
assert.ok(result === null || result !== undefined);
|
|
});
|
|
});
|
|
});
|