Skip to content

Commit

Permalink
refactor: moved methods to the right folder, added more interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
thorsten committed Jan 27, 2025
1 parent 62d34d9 commit fc07f79
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 129 deletions.
33 changes: 27 additions & 6 deletions phpmyfaq/admin/assets/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
*/

import { Response } from '../interfaces';
import { UserData } from '../interfaces/userData';

export const fetchUsers = async (userName: string): Promise<Response | undefined> => {
export const fetchUsers = async (userName: string): Promise<Response> => {
try {
const response = await fetch(`./api/user/users?filter=${userName}`, {
method: 'GET',
Expand All @@ -33,7 +34,7 @@ export const fetchUsers = async (userName: string): Promise<Response | undefined
}
};

export const fetchUserData = async (userId: string): Promise<Response | undefined> => {
export const fetchUserData = async (userId: string): Promise<UserData> => {
try {
const response = await fetch(`./api/user/data/${userId}`, {
method: 'GET',
Expand All @@ -51,7 +52,7 @@ export const fetchUserData = async (userId: string): Promise<Response | undefine
}
};

export const fetchUserRights = async (userId: string): Promise<Response | undefined> => {
export const fetchUserRights = async (userId: string): Promise<number[]> => {
try {
const response = await fetch(`./api/user/permissions/${userId}`, {
method: 'GET',
Expand All @@ -69,7 +70,7 @@ export const fetchUserRights = async (userId: string): Promise<Response | undefi
}
};

export const fetchAllUsers = async (): Promise<Response | undefined> => {
export const fetchAllUsers = async (): Promise<Response> => {
try {
const response = await fetch('./api/user/users', {
method: 'GET',
Expand Down Expand Up @@ -114,7 +115,7 @@ export const overwritePassword = async (
}
};

export const postUserData = async (url: string = '', data: Record<string, any> = {}): Promise<Response | undefined> => {
export const postUserData = async (url: string = '', data: Record<string, any> = {}): Promise<Response> => {
try {
const response = await fetch(url, {
method: 'POST',
Expand All @@ -133,7 +134,27 @@ export const postUserData = async (url: string = '', data: Record<string, any> =
}
};

export const deleteUser = async (userId: string, csrfToken: string): Promise<Response | undefined> => {
export const activateUser = async (userId: string, csrfToken: string): Promise<Response> => {
try {
const response = await fetch('./api/user/activate', {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({
csrfToken: csrfToken,
userId: userId,
}),
});

return await response.json();
} catch (error) {
throw error;
}
};

export const deleteUser = async (userId: string, csrfToken: string): Promise<Response> => {
try {
const response = await fetch('./api/user/delete', {
method: 'DELETE',
Expand Down
2 changes: 2 additions & 0 deletions phpmyfaq/admin/assets/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './elasticsearch';
export * from './instance';
export * from './response';
export * from './stopWord';
export * from './userAutocomplete';
export * from './userData';
4 changes: 4 additions & 0 deletions phpmyfaq/admin/assets/src/interfaces/userAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface UserAutocomplete {
label: string;
value: string;
}
12 changes: 12 additions & 0 deletions phpmyfaq/admin/assets/src/interfaces/userData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface UserData {
userId: string;
login: string;
displayName: string;
email: string;
status: string;
lastModified: string;
authSource: string;
twoFactorEnabled: string;
isSuperadmin: string;
json(): Promise<UserData>;
}
74 changes: 74 additions & 0 deletions phpmyfaq/admin/assets/src/user/autocomplete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import autocomplete from 'autocompleter';
import { updateUser } from './users';
import { fetchUsers } from '../api';
import { addElement } from '../../../../assets/src/utils';
import './autocomplete'; // Ensure the event listener is registered

vi.mock('autocompleter', () => ({
__esModule: true,
default: vi.fn(),
}));

vi.mock('./users', () => ({
updateUser: vi.fn(),
}));

vi.mock('../api', () => ({
fetchUsers: vi.fn(),
}));

vi.mock('../../../../assets/src/utils', () => ({
addElement: vi.fn(() => document.createElement('div')),
}));

describe('User Autocomplete', () => {
beforeEach(() => {
document.body.innerHTML = `
<input id="pmf-user-list-autocomplete" />
`;
});

test('should initialize autocomplete on DOMContentLoaded', () => {
const mockAutocomplete = vi.fn();
(autocomplete as unknown as vi.Mock).mockImplementation(mockAutocomplete);

document.dispatchEvent(new Event('DOMContentLoaded'));

expect(mockAutocomplete).toHaveBeenCalled();
});

test('should call updateUser on item select', async () => {
const mockItem = { label: 'John Doe', value: '1' };
const input = document.getElementById('pmf-user-list-autocomplete') as HTMLInputElement;

const onSelect = (autocomplete as unknown as vi.Mock).mock.calls[0][0].onSelect;
await onSelect(mockItem, input);

expect(updateUser).toHaveBeenCalledWith('1');
});

test('should fetch and filter users', async () => {
const mockUsers = [{ label: 'John Doe', value: '1' }];
(fetchUsers as unknown as vi.Mock).mockResolvedValue(mockUsers);

const fetch = (autocomplete as unknown as vi.Mock).mock.calls[0][0].fetch;
const callback = vi.fn();
await fetch('john', callback);

expect(fetchUsers).toHaveBeenCalledWith('john');
expect(callback).toHaveBeenCalledWith(mockUsers);
});

test('should render user suggestions', () => {
const mockItem = { label: 'John Doe', value: '1' };
const render = (autocomplete as unknown as vi.Mock).mock.calls[0][0].render;
const result = render(mockItem, 'john');

expect(addElement).toHaveBeenCalledWith('div', {
classList: 'pmf-user-list-result border',
innerHTML: '<strong>John</strong> Doe',
});
expect(result).toBeInstanceOf(HTMLDivElement);
});
});
8 changes: 2 additions & 6 deletions phpmyfaq/admin/assets/src/user/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,9 @@ import autocomplete, { AutocompleteItem } from 'autocompleter';
import { updateUser } from './users';
import { fetchUsers } from '../api';
import { addElement } from '../../../../assets/src/utils';
import { UserAutocomplete } from '../interfaces';

interface User {
label: string;
value: string;
}

type UserSuggestion = User & AutocompleteItem;
type UserSuggestion = UserAutocomplete & AutocompleteItem;

document.addEventListener('DOMContentLoaded', () => {
const autoComplete = document.getElementById('pmf-user-list-autocomplete') as HTMLInputElement;
Expand Down
54 changes: 54 additions & 0 deletions phpmyfaq/admin/assets/src/user/user-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { activateUser, deleteUser, overwritePassword, postUserData } from '../api';

global.fetch = vi.fn();

describe('User API', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should overwrite password', async () => {
const mockResponse = { success: true };
(fetch as vi.Mock).mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse),
});

const response = await overwritePassword('csrfToken', 'userId', 'newPassword', 'passwordRepeat');
expect(fetch).toHaveBeenCalledWith('./api/user/overwrite-password', expect.any(Object));
expect(response).toEqual(mockResponse);
});

it('should post user data', async () => {
const mockResponse = { success: true };
(fetch as vi.Mock).mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse),
});

const response = await postUserData('url', { key: 'value' });
expect(fetch).toHaveBeenCalledWith('url', expect.any(Object));
expect(response).toEqual(mockResponse);
});

it('should activate user', async () => {
const mockResponse = { success: true };
(fetch as vi.Mock).mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse),
});

const response = await activateUser('userId', 'csrfToken');
expect(fetch).toHaveBeenCalledWith('./api/user/activate', expect.any(Object));
expect(response).toEqual(mockResponse);
});

it('should delete user', async () => {
const mockResponse = { success: true };
(fetch as vi.Mock).mockResolvedValue({
json: vi.fn().mockResolvedValue(mockResponse),
});

const response = await deleteUser('userId', 'csrfToken');
expect(fetch).toHaveBeenCalledWith('./api/user/delete', expect.any(Object));
expect(response).toEqual(mockResponse);
});
});
65 changes: 22 additions & 43 deletions phpmyfaq/admin/assets/src/user/user-list.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/**
* Functions for handling user management
*
* @todo move fetch() functionality to api functions
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
Expand All @@ -16,41 +14,9 @@
*/

import { addElement, pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
import { deleteUser } from '../api';
import { activateUser, deleteUser } from '../api';
import { Modal } from 'bootstrap';

const activateUser = async (userId: string, csrfToken: string): Promise<void> => {
try {
const response = await fetch('./api/user/activate', {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({
csrfToken: csrfToken,
userId: userId,
}),
});

if (response.status === 200) {
await response.json();
const icon = document.querySelector(`.icon_user_id_${userId}`) as HTMLElement;
icon.classList.remove('bi-ban');
icon.classList.add('bi-check-circle-o');
const button = document.getElementById(`btn_activate_user_id_${userId}`) as HTMLElement;
button.remove();
} else {
throw new Error('Network response was not ok.');
}
} catch (error) {
const message = document.getElementById('pmf-user-message') as HTMLElement;
message.insertAdjacentElement(
'afterend',
addElement('div', { classList: 'alert alert-danger', innerText: (error as Error).message })
);
}
};
import { Response } from '../interfaces';

export const handleUserList = (): void => {
const activateButtons = document.querySelectorAll('.btn-activate-user');
Expand All @@ -65,7 +31,21 @@ export const handleUserList = (): void => {
const csrfToken = target.getAttribute('data-csrf-token')!;
const userId = target.getAttribute('data-user-id')!;

await activateUser(userId, csrfToken);
const response = (await activateUser(userId, csrfToken)) as unknown as Response;

if (typeof response.success === 'string') {
const icon = document.querySelector(`.icon_user_id_${userId}`) as HTMLElement;
icon.classList.remove('bi-ban');
icon.classList.add('bi-check-circle-o');
const button = document.getElementById(`btn_activate_user_id_${userId}`) as HTMLElement;
button.remove();
} else {
const message = document.getElementById('pmf-user-message') as HTMLElement;
message.insertAdjacentElement(
'afterend',
addElement('div', { classList: 'alert alert-danger', innerText: response.error })
);
}
});
});
}
Expand Down Expand Up @@ -94,15 +74,14 @@ export const handleUserList = (): void => {
if (source.value === 'user-list') {
const userId = (document.getElementById('pmf-user-id-delete') as HTMLInputElement).value;
const csrfToken = (document.getElementById('csrf-token-delete-user') as HTMLInputElement).value;
const response = await deleteUser(userId, csrfToken);
const json = await response.json();
if (json.success) {
pushNotification(json.success);
const response = (await deleteUser(userId, csrfToken)) as unknown as Response;
if (response.success) {
pushNotification(response.success);
const row = document.getElementById('row_user_id_' + userId) as HTMLElement;
row.remove();
}
if (json.error) {
pushErrorNotification(json.error);
if (response.error) {
pushErrorNotification(response.error);
}
}
});
Expand Down
Loading

0 comments on commit fc07f79

Please sign in to comment.