Skip to content

Commit fc07f79

Browse files
committed
refactor: moved methods to the right folder, added more interfaces
1 parent 62d34d9 commit fc07f79

File tree

11 files changed

+259
-129
lines changed

11 files changed

+259
-129
lines changed

phpmyfaq/admin/assets/src/api/user.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
*/
1515

1616
import { Response } from '../interfaces';
17+
import { UserData } from '../interfaces/userData';
1718

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

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

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

72-
export const fetchAllUsers = async (): Promise<Response | undefined> => {
73+
export const fetchAllUsers = async (): Promise<Response> => {
7374
try {
7475
const response = await fetch('./api/user/users', {
7576
method: 'GET',
@@ -114,7 +115,7 @@ export const overwritePassword = async (
114115
}
115116
};
116117

117-
export const postUserData = async (url: string = '', data: Record<string, any> = {}): Promise<Response | undefined> => {
118+
export const postUserData = async (url: string = '', data: Record<string, any> = {}): Promise<Response> => {
118119
try {
119120
const response = await fetch(url, {
120121
method: 'POST',
@@ -133,7 +134,27 @@ export const postUserData = async (url: string = '', data: Record<string, any> =
133134
}
134135
};
135136

136-
export const deleteUser = async (userId: string, csrfToken: string): Promise<Response | undefined> => {
137+
export const activateUser = async (userId: string, csrfToken: string): Promise<Response> => {
138+
try {
139+
const response = await fetch('./api/user/activate', {
140+
method: 'POST',
141+
headers: {
142+
Accept: 'application/json, text/plain, */*',
143+
'Content-Type': 'application/json',
144+
},
145+
body: JSON.stringify({
146+
csrfToken: csrfToken,
147+
userId: userId,
148+
}),
149+
});
150+
151+
return await response.json();
152+
} catch (error) {
153+
throw error;
154+
}
155+
};
156+
157+
export const deleteUser = async (userId: string, csrfToken: string): Promise<Response> => {
137158
try {
138159
const response = await fetch('./api/user/delete', {
139160
method: 'DELETE',

phpmyfaq/admin/assets/src/interfaces/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './elasticsearch';
22
export * from './instance';
33
export * from './response';
44
export * from './stopWord';
5+
export * from './userAutocomplete';
6+
export * from './userData';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface UserAutocomplete {
2+
label: string;
3+
value: string;
4+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface UserData {
2+
userId: string;
3+
login: string;
4+
displayName: string;
5+
email: string;
6+
status: string;
7+
lastModified: string;
8+
authSource: string;
9+
twoFactorEnabled: string;
10+
isSuperadmin: string;
11+
json(): Promise<UserData>;
12+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, test, vi, beforeEach } from 'vitest';
2+
import autocomplete from 'autocompleter';
3+
import { updateUser } from './users';
4+
import { fetchUsers } from '../api';
5+
import { addElement } from '../../../../assets/src/utils';
6+
import './autocomplete'; // Ensure the event listener is registered
7+
8+
vi.mock('autocompleter', () => ({
9+
__esModule: true,
10+
default: vi.fn(),
11+
}));
12+
13+
vi.mock('./users', () => ({
14+
updateUser: vi.fn(),
15+
}));
16+
17+
vi.mock('../api', () => ({
18+
fetchUsers: vi.fn(),
19+
}));
20+
21+
vi.mock('../../../../assets/src/utils', () => ({
22+
addElement: vi.fn(() => document.createElement('div')),
23+
}));
24+
25+
describe('User Autocomplete', () => {
26+
beforeEach(() => {
27+
document.body.innerHTML = `
28+
<input id="pmf-user-list-autocomplete" />
29+
`;
30+
});
31+
32+
test('should initialize autocomplete on DOMContentLoaded', () => {
33+
const mockAutocomplete = vi.fn();
34+
(autocomplete as unknown as vi.Mock).mockImplementation(mockAutocomplete);
35+
36+
document.dispatchEvent(new Event('DOMContentLoaded'));
37+
38+
expect(mockAutocomplete).toHaveBeenCalled();
39+
});
40+
41+
test('should call updateUser on item select', async () => {
42+
const mockItem = { label: 'John Doe', value: '1' };
43+
const input = document.getElementById('pmf-user-list-autocomplete') as HTMLInputElement;
44+
45+
const onSelect = (autocomplete as unknown as vi.Mock).mock.calls[0][0].onSelect;
46+
await onSelect(mockItem, input);
47+
48+
expect(updateUser).toHaveBeenCalledWith('1');
49+
});
50+
51+
test('should fetch and filter users', async () => {
52+
const mockUsers = [{ label: 'John Doe', value: '1' }];
53+
(fetchUsers as unknown as vi.Mock).mockResolvedValue(mockUsers);
54+
55+
const fetch = (autocomplete as unknown as vi.Mock).mock.calls[0][0].fetch;
56+
const callback = vi.fn();
57+
await fetch('john', callback);
58+
59+
expect(fetchUsers).toHaveBeenCalledWith('john');
60+
expect(callback).toHaveBeenCalledWith(mockUsers);
61+
});
62+
63+
test('should render user suggestions', () => {
64+
const mockItem = { label: 'John Doe', value: '1' };
65+
const render = (autocomplete as unknown as vi.Mock).mock.calls[0][0].render;
66+
const result = render(mockItem, 'john');
67+
68+
expect(addElement).toHaveBeenCalledWith('div', {
69+
classList: 'pmf-user-list-result border',
70+
innerHTML: '<strong>John</strong> Doe',
71+
});
72+
expect(result).toBeInstanceOf(HTMLDivElement);
73+
});
74+
});

phpmyfaq/admin/assets/src/user/autocomplete.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@ import autocomplete, { AutocompleteItem } from 'autocompleter';
1717
import { updateUser } from './users';
1818
import { fetchUsers } from '../api';
1919
import { addElement } from '../../../../assets/src/utils';
20+
import { UserAutocomplete } from '../interfaces';
2021

21-
interface User {
22-
label: string;
23-
value: string;
24-
}
25-
26-
type UserSuggestion = User & AutocompleteItem;
22+
type UserSuggestion = UserAutocomplete & AutocompleteItem;
2723

2824
document.addEventListener('DOMContentLoaded', () => {
2925
const autoComplete = document.getElementById('pmf-user-list-autocomplete') as HTMLInputElement;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { activateUser, deleteUser, overwritePassword, postUserData } from '../api';
3+
4+
global.fetch = vi.fn();
5+
6+
describe('User API', () => {
7+
beforeEach(() => {
8+
vi.clearAllMocks();
9+
});
10+
11+
it('should overwrite password', async () => {
12+
const mockResponse = { success: true };
13+
(fetch as vi.Mock).mockResolvedValue({
14+
json: vi.fn().mockResolvedValue(mockResponse),
15+
});
16+
17+
const response = await overwritePassword('csrfToken', 'userId', 'newPassword', 'passwordRepeat');
18+
expect(fetch).toHaveBeenCalledWith('./api/user/overwrite-password', expect.any(Object));
19+
expect(response).toEqual(mockResponse);
20+
});
21+
22+
it('should post user data', async () => {
23+
const mockResponse = { success: true };
24+
(fetch as vi.Mock).mockResolvedValue({
25+
json: vi.fn().mockResolvedValue(mockResponse),
26+
});
27+
28+
const response = await postUserData('url', { key: 'value' });
29+
expect(fetch).toHaveBeenCalledWith('url', expect.any(Object));
30+
expect(response).toEqual(mockResponse);
31+
});
32+
33+
it('should activate user', async () => {
34+
const mockResponse = { success: true };
35+
(fetch as vi.Mock).mockResolvedValue({
36+
json: vi.fn().mockResolvedValue(mockResponse),
37+
});
38+
39+
const response = await activateUser('userId', 'csrfToken');
40+
expect(fetch).toHaveBeenCalledWith('./api/user/activate', expect.any(Object));
41+
expect(response).toEqual(mockResponse);
42+
});
43+
44+
it('should delete user', async () => {
45+
const mockResponse = { success: true };
46+
(fetch as vi.Mock).mockResolvedValue({
47+
json: vi.fn().mockResolvedValue(mockResponse),
48+
});
49+
50+
const response = await deleteUser('userId', 'csrfToken');
51+
expect(fetch).toHaveBeenCalledWith('./api/user/delete', expect.any(Object));
52+
expect(response).toEqual(mockResponse);
53+
});
54+
});

phpmyfaq/admin/assets/src/user/user-list.ts

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/**
22
* Functions for handling user management
33
*
4-
* @todo move fetch() functionality to api functions
5-
*
64
* This Source Code Form is subject to the terms of the Mozilla Public License,
75
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
86
* obtain one at https://mozilla.org/MPL/2.0/.
@@ -16,41 +14,9 @@
1614
*/
1715

1816
import { addElement, pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
19-
import { deleteUser } from '../api';
17+
import { activateUser, deleteUser } from '../api';
2018
import { Modal } from 'bootstrap';
21-
22-
const activateUser = async (userId: string, csrfToken: string): Promise<void> => {
23-
try {
24-
const response = await fetch('./api/user/activate', {
25-
method: 'POST',
26-
headers: {
27-
Accept: 'application/json, text/plain, */*',
28-
'Content-Type': 'application/json',
29-
},
30-
body: JSON.stringify({
31-
csrfToken: csrfToken,
32-
userId: userId,
33-
}),
34-
});
35-
36-
if (response.status === 200) {
37-
await response.json();
38-
const icon = document.querySelector(`.icon_user_id_${userId}`) as HTMLElement;
39-
icon.classList.remove('bi-ban');
40-
icon.classList.add('bi-check-circle-o');
41-
const button = document.getElementById(`btn_activate_user_id_${userId}`) as HTMLElement;
42-
button.remove();
43-
} else {
44-
throw new Error('Network response was not ok.');
45-
}
46-
} catch (error) {
47-
const message = document.getElementById('pmf-user-message') as HTMLElement;
48-
message.insertAdjacentElement(
49-
'afterend',
50-
addElement('div', { classList: 'alert alert-danger', innerText: (error as Error).message })
51-
);
52-
}
53-
};
19+
import { Response } from '../interfaces';
5420

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

68-
await activateUser(userId, csrfToken);
34+
const response = (await activateUser(userId, csrfToken)) as unknown as Response;
35+
36+
if (typeof response.success === 'string') {
37+
const icon = document.querySelector(`.icon_user_id_${userId}`) as HTMLElement;
38+
icon.classList.remove('bi-ban');
39+
icon.classList.add('bi-check-circle-o');
40+
const button = document.getElementById(`btn_activate_user_id_${userId}`) as HTMLElement;
41+
button.remove();
42+
} else {
43+
const message = document.getElementById('pmf-user-message') as HTMLElement;
44+
message.insertAdjacentElement(
45+
'afterend',
46+
addElement('div', { classList: 'alert alert-danger', innerText: response.error })
47+
);
48+
}
6949
});
7050
});
7151
}
@@ -94,15 +74,14 @@ export const handleUserList = (): void => {
9474
if (source.value === 'user-list') {
9575
const userId = (document.getElementById('pmf-user-id-delete') as HTMLInputElement).value;
9676
const csrfToken = (document.getElementById('csrf-token-delete-user') as HTMLInputElement).value;
97-
const response = await deleteUser(userId, csrfToken);
98-
const json = await response.json();
99-
if (json.success) {
100-
pushNotification(json.success);
77+
const response = (await deleteUser(userId, csrfToken)) as unknown as Response;
78+
if (response.success) {
79+
pushNotification(response.success);
10180
const row = document.getElementById('row_user_id_' + userId) as HTMLElement;
10281
row.remove();
10382
}
104-
if (json.error) {
105-
pushErrorNotification(json.error);
83+
if (response.error) {
84+
pushErrorNotification(response.error);
10685
}
10786
}
10887
});

0 commit comments

Comments
 (0)