Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"@iconscout/react-unicons": "^1.1.6",
"@internxt/css-config": "1.1.0",
"@internxt/lib": "1.4.1",
"@internxt/sdk": "=1.16.4",
"@internxt/sdk": "=1.17.1",
"@internxt/ui": "=0.1.15",
"@phosphor-icons/react": "^2.1.7",
"@popperjs/core": "^2.11.6",
Expand Down
195 changes: 195 additions & 0 deletions src/app/drive/components/ShareDialog/hooks/useAccessRequests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { renderHook } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { useAccessRequests } from './useAccessRequests';
import { useAppDispatch } from 'app/store/hooks';
import shareService from 'app/share/services/share.service';
import { localStorageService, errorService } from 'services';
import notificationsService, { ToastType } from 'app/notifications/services/notifications.service';
import { aes, stringUtils } from '@internxt/lib';
import { sharedActions } from 'app/store/slices/sharedLinks';
import { getUser } from 'testUtils/fixtures/users.fixtures';
import { getCastedError } from 'testUtils/fixtures/drive.fixtures';

vi.mock('app/store/hooks', () => ({
useAppDispatch: vi.fn(),
}));

vi.mock('i18next', () => ({
t: (key: string) => key,
}));

vi.mock('app/share/services/share.service', () => ({
default: {
acceptSharedFolderInvite: vi.fn(),
declineSharedFolderInvite: vi.fn(),
},
}));

vi.mock('services', () => ({
localStorageService: { getUser: vi.fn() },
errorService: { castError: vi.fn() },
}));

vi.mock('app/notifications/services/notifications.service', () => ({
default: { show: vi.fn() },
ToastType: { Error: 'error', Success: 'success' },
}));

vi.mock('@internxt/lib', () => ({
aes: { encrypt: vi.fn() },
stringUtils: { generateRandomStringUrlSafe: vi.fn() },
}));

vi.mock('app/store/slices/sharedLinks', () => ({
sharedActions: { popAccessRequest: vi.fn() },
}));

describe('Access Requests - Custom Hook', () => {
const mockDispatch = vi.fn();
const mockPopAccessRequestAction = { type: 'shared/popAccessRequest', payload: '' };

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useAppDispatch).mockReturnValue(mockDispatch);
vi.spyOn(sharedActions, 'popAccessRequest').mockReturnValue(mockPopAccessRequestAction as any);
});

describe('Accepting the access request', () => {
it('When a user accepts a folder access request, then the mnemonic is encrypted and the invite is accepted with the correct payload', async () => {
const invitationId = 'invite-111';
const roleId = 'role-editor';
const user = getUser();
const randomCode = 'rnd8code';
const encryptedMnemonic = 'encrypted-mnemonic-value';

vi.spyOn(localStorageService, 'getUser').mockReturnValue(user as any);
const generateRandomStringSpy = vi.spyOn(stringUtils, 'generateRandomStringUrlSafe').mockReturnValue(randomCode);
const aesEncryptSpy = vi.spyOn(aes, 'encrypt').mockReturnValue(encryptedMnemonic);
const acceptSharedFolderInviteSpy = vi
.spyOn(shareService, 'acceptSharedFolderInvite')
.mockResolvedValue(undefined as any);

const { result } = renderHook(() => useAccessRequests());

await result.current.onAcceptAccessRequest(invitationId, { roleId });

expect(generateRandomStringSpy).toHaveBeenCalledWith(8);
expect(aesEncryptSpy).toHaveBeenCalledWith(user.mnemonic, randomCode);
expect(acceptSharedFolderInviteSpy).toHaveBeenCalledWith({
invitationId,
acceptInvite: {
encryptionKey: encryptedMnemonic,
encryptionAlgorithm: 'inxt-v2',
roleId,
},
});
});

it('When a folder access request is accepted successfully, then the invitation is removed from the pending list', async () => {
const invitationId = 'invite-222';
const roleId = 'role-viewer';

vi.spyOn(localStorageService, 'getUser').mockReturnValue(getUser() as any);
vi.spyOn(stringUtils, 'generateRandomStringUrlSafe').mockReturnValue('abcd1234');
vi.spyOn(aes, 'encrypt').mockReturnValue('enc-mnemonic');
vi.spyOn(shareService, 'acceptSharedFolderInvite').mockResolvedValue(undefined as any);
const popAccessRequestSpy = vi
.spyOn(sharedActions, 'popAccessRequest')
.mockReturnValue(mockPopAccessRequestAction as any);

const { result } = renderHook(() => useAccessRequests());

await result.current.onAcceptAccessRequest(invitationId, { roleId });

expect(popAccessRequestSpy).toHaveBeenCalledWith(invitationId);
expect(mockDispatch).toHaveBeenCalledWith(mockPopAccessRequestAction);
});

it('When the share service rejects while accepting a request, then an error notification is displayed and the invitation is NOT removed from the list', async () => {
const invitationId = 'invite-333';
const roleId = 'role-owner';
const networkError = new Error('Network failure');
const castedError = getCastedError({ requestId: 'req-999' });

vi.spyOn(localStorageService, 'getUser').mockReturnValue(getUser() as any);
vi.spyOn(stringUtils, 'generateRandomStringUrlSafe').mockReturnValue('xy8zcode');
vi.spyOn(aes, 'encrypt').mockReturnValue('enc-mnemonic');
vi.spyOn(shareService, 'acceptSharedFolderInvite').mockRejectedValue(networkError);
const castErrorSpy = vi.spyOn(errorService, 'castError').mockReturnValue(castedError as any);
const notificationServiceSpy = vi.spyOn(notificationsService, 'show');
const popAccessRequestSpy = vi
.spyOn(sharedActions, 'popAccessRequest')
.mockReturnValue(mockPopAccessRequestAction as any);

const { result } = renderHook(() => useAccessRequests());

await result.current.onAcceptAccessRequest(invitationId, { roleId });

expect(castErrorSpy).toHaveBeenCalledWith(networkError);
expect(notificationServiceSpy).toHaveBeenCalledWith({
text: 'notificationMessages.errorAcceptingAccessRequest',
type: ToastType.Error,
requestId: castedError.requestId,
});
expect(popAccessRequestSpy).not.toHaveBeenCalled();
expect(mockDispatch).not.toHaveBeenCalled();
});
});

describe('Declining the access request', () => {
it('When a user declines a folder access request, then the decline is sent to the share service with the correct invitation ID', async () => {
const invitationId = 'invite-444';

const declineSharedFolderInviteSpy = vi
.spyOn(shareService, 'declineSharedFolderInvite')
.mockResolvedValue(undefined as any);

const { result } = renderHook(() => useAccessRequests());

await result.current.onDeclineAccessRequest(invitationId);

expect(declineSharedFolderInviteSpy).toHaveBeenCalledWith({ invitationId });
});

it('When a folder access request is declined successfully, then the invitation is removed from the pending list', async () => {
const invitationId = 'invite-555';

vi.spyOn(shareService, 'declineSharedFolderInvite').mockResolvedValue(undefined as any);
const popAccessRequestSpy = vi
.spyOn(sharedActions, 'popAccessRequest')
.mockReturnValue(mockPopAccessRequestAction as any);

const { result } = renderHook(() => useAccessRequests());

await result.current.onDeclineAccessRequest(invitationId);

expect(popAccessRequestSpy).toHaveBeenCalledWith(invitationId);
expect(mockDispatch).toHaveBeenCalledWith(mockPopAccessRequestAction);
});

it('When the share service rejects while declining a request, then an error notification is displayed and the invitation is NOT removed from the list', async () => {
const invitationId = 'invite-666';
const networkError = new Error('Service unavailable');
const castedError = getCastedError({ requestId: 'req-777' });

vi.spyOn(shareService, 'declineSharedFolderInvite').mockRejectedValue(networkError);
const notificationServiceSpy = vi.spyOn(notificationsService, 'show');
const castErrorSpy = vi.spyOn(errorService, 'castError').mockReturnValue(castedError as any);
const popAccessRequestSpy = vi.spyOn(sharedActions, 'popAccessRequest');

const { result } = renderHook(() => useAccessRequests());

await result.current.onDeclineAccessRequest(invitationId);

expect(castErrorSpy).toHaveBeenCalledWith(networkError);
expect(notificationServiceSpy).toHaveBeenCalledWith({
text: 'notificationMessages.errorDecliningAccessRequest',
type: ToastType.Error,
requestId: castedError.requestId,
});
expect(popAccessRequestSpy).not.toHaveBeenCalled();
expect(mockDispatch).not.toHaveBeenCalled();
});
});
});
69 changes: 69 additions & 0 deletions src/app/drive/components/ShareDialog/hooks/useAccessRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { aes, stringUtils } from '@internxt/lib';
import { AcceptInvitationToSharedFolderPayload } from '@internxt/sdk/dist/drive/share/types';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import notificationsService, { ToastType } from 'app/notifications/services/notifications.service';
import shareService from 'app/share/services/share.service';
import { useAppDispatch } from 'app/store/hooks';
import { sharedActions } from 'app/store/slices/sharedLinks';
import { t } from 'i18next';
import { errorService, localStorageService } from 'services';

export const useAccessRequests = () => {
const dispatch = useAppDispatch();
const removeAccessRequestFromList = (invitationId: string) => {
dispatch(sharedActions.popAccessRequest(invitationId));
};

const onAcceptAccessRequest = async (
invitationId: string,
payload: Pick<AcceptInvitationToSharedFolderPayload, 'roleId'>,
) => {
try {
const user = localStorageService.getUser() as UserSettings;
const { mnemonic } = user;
const code = stringUtils.generateRandomStringUrlSafe(8);
const encryptedMnemonic = aes.encrypt(mnemonic, code);

await shareService.acceptSharedFolderInvite({
invitationId: invitationId,
acceptInvite: {
encryptionKey: encryptedMnemonic,
encryptionAlgorithm: 'inxt-v2',
roleId: payload.roleId,
},
});

removeAccessRequestFromList(invitationId);
} catch (error) {
const castedError = errorService.castError(error);
console.error('[ACCESS REQUEST] Error while accepting an access request: ', castedError);
notificationsService.show({
text: t('notificationMessages.errorAcceptingAccessRequest'),
type: ToastType.Error,
requestId: castedError.requestId,
});
}
};

const onDeclineAccessRequest = async (invitationId: string): Promise<void> => {
try {
await shareService.declineSharedFolderInvite({
invitationId,
});
removeAccessRequestFromList(invitationId);
} catch (error) {
const castedError = errorService.castError(error);
console.error('[ACCESS REQUEST] Error while declining an access request: ', castedError);
notificationsService.show({
text: t('notificationMessages.errorDecliningAccessRequest'),
type: ToastType.Error,
requestId: castedError.requestId,
});
}
};

return {
onAcceptAccessRequest,
onDeclineAccessRequest,
};
};
5 changes: 4 additions & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,10 @@
"errorWhileUpdatingWorkspaceMembers": "Beim Aktualisieren der Mitglieder ist ein Fehler aufgetreten",
"errorWhileFetchingCurrentWorkspaceMembers": "Beim Abrufen der Mitglieder des Arbeitsbereichs ist ein Fehler aufgetreten",
"newMembersCannotBeLessThanTheExistentOnesError": "Die ausgewählten Mitglieder dürfen nicht weniger sein als die aktuellen Mitglieder des Arbeitsbereichs",
"reachedFileSizeLimit": "Dateigrößenlimit überschritten"
"reachedFileSizeLimit": "Dateigrößenlimit überschritten",
"errorAcceptingAccessRequest": "Beim Akzeptieren der Zugriffsanfrage ist ein Fehler aufgetreten",
"errorDecliningAccessRequest": "Beim Ablehnen der Zugriffsanfrage ist ein Fehler aufgetreten",
"errorGettingAccessRequests": "Beim Abrufen der Zugriffsanfragen ist ein Fehler aufgetreten"
},
"actions": {
"undo": "Rückgängig machen",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,10 @@
"errorWhileUpdatingWorkspaceMembers": "Something went wrong while updating the members",
"errorWhileFetchingCurrentWorkspaceMembers": "Something went wrong while fetching workspace members",
"newMembersCannotBeLessThanTheExistentOnesError": "Selected members cannot be fewer than current workspace members",
"reachedFileSizeLimit": "File size limit exceeded"
"reachedFileSizeLimit": "File size limit exceeded",
"errorAcceptingAccessRequest": "Something went wrong while accepting the access request",
"errorDecliningAccessRequest": "Something went wrong while declining the access request",
"errorGettingAccessRequests": "Something went wrong while getting access requests"
},
"actions": {
"undo": "Undo",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1529,7 +1529,10 @@
"errorWhileUpdatingWorkspaceMembers": "Ocurrió un error al actualizar los miembros",
"errorWhileFetchingCurrentWorkspaceMembers": "Ocurrió un error al obtener los miembros del espacio de trabajo",
"newMembersCannotBeLessThanTheExistentOnesError": "Los miembros seleccionados no pueden ser menos que los miembros actuales del espacio de trabajo",
"reachedFileSizeLimit": "Límite de tamaño de archivo superado"
"reachedFileSizeLimit": "Límite de tamaño de archivo superado",
"errorAcceptingAccessRequest": "Algo salió mal al aceptar la solicitud de acceso",
"errorDecliningAccessRequest": "Algo salió mal al rechazar la solicitud de acceso",
"errorGettingAccessRequests": "Algo salió mal al obtener las solicitudes de acceso"
},
"success": {
"passwordChanged": "Contraseña actualizada correctamente",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,10 @@
"errorWhileUpdatingWorkspaceMembers": "Une erreur s'est produite lors de la mise à jour des membres",
"errorWhileFetchingCurrentWorkspaceMembers": "Une erreur s'est produite lors de la récupération des membres de l'espace de travail",
"newMembersCannotBeLessThanTheExistentOnesError": "Les membres sélectionnés ne peuvent pas être inférieurs au nombre de membres actuels de l'espace de travail",
"reachedFileSizeLimit": "Limite de taille de fichier dépassée"
"reachedFileSizeLimit": "Limite de taille de fichier dépassée",
"errorAcceptingAccessRequest": "Une erreur s'est produite lors de l'acceptation de la demande d'accès",
"errorDecliningAccessRequest": "Une erreur s'est produite lors du refus de la demande d'accès",
"errorGettingAccessRequests": "Une erreur s'est produite lors de la récupération des demandes d'accès"
},
"actions": {
"undo": "Annuler",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1597,7 +1597,10 @@
"errorWhileUpdatingWorkspaceMembers": "Si è verificato un errore durante l'aggiornamento dei membri",
"errorWhileFetchingCurrentWorkspaceMembers": "Si è verificato un errore durante il recupero dei membri dello spazio di lavoro",
"newMembersCannotBeLessThanTheExistentOnesError": "I membri selezionati non possono essere meno dei membri attuali dello spazio di lavoro",
"reachedFileSizeLimit": "Limite di dimensione del file superato"
"reachedFileSizeLimit": "Limite di dimensione del file superato",
"errorAcceptingAccessRequest": "Si è verificato un errore durante l'accettazione della richiesta di accesso",
"errorDecliningAccessRequest": "Si è verificato un errore durante il rifiuto della richiesta di accesso",
"errorGettingAccessRequests": "Si è verificato un errore durante il recupero delle richieste di accesso"
},
"actions": {
"undo": "Annulla",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1505,7 +1505,10 @@
"errorWhileUpdatingWorkspaceMembers": "Произошла ошибка при обновлении участников",
"errorWhileFetchingCurrentWorkspaceMembers": "Произошла ошибка при получении участников рабочего пространства",
"newMembersCannotBeLessThanTheExistentOnesError": "Выбранных участников не может быть меньше, чем текущих участников рабочего пространства",
"reachedFileSizeLimit": "Превышен лимит размера файла"
"reachedFileSizeLimit": "Превышен лимит размера файла",
"errorAcceptingAccessRequest": "Произошла ошибка при принятии запроса на доступ",
"errorDecliningAccessRequest": "Произошла ошибка при отклонении запроса на доступ",
"errorGettingAccessRequests": "Произошла ошибка при получении запросов на доступ"
},
"actions": {
"undo": "Отменить",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -1496,7 +1496,10 @@
"errorWhileUpdatingWorkspaceMembers": "更新成員時出了點問題",
"errorWhileFetchingCurrentWorkspaceMembers": "獲取工作區成員時出了點問題",
"newMembersCannotBeLessThanTheExistentOnesError": "選擇的成員數量不能少於當前工作區的成員數量。",
"reachedFileSizeLimit": "已超過檔案大小限制"
"reachedFileSizeLimit": "已超過檔案大小限制",
"errorAcceptingAccessRequest": "接受存取請求時發生錯誤",
"errorDecliningAccessRequest": "拒絕存取請求時發生錯誤",
"errorGettingAccessRequests": "獲取存取請求時發生錯誤"
},
"actions": {
"undo": "撤銷",
Expand Down
5 changes: 4 additions & 1 deletion src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1531,7 +1531,10 @@
"errorWhileUpdatingWorkspaceMembers": "更新成员时出了点问题",
"errorWhileFetchingCurrentWorkspaceMembers": "获取工作区成员时出了点问题",
"newMembersCannotBeLessThanTheExistentOnesError": "选择的成员数量不能少于当前工作区的成员数量。",
"reachedFileSizeLimit": "已超过文件大小限制"
"reachedFileSizeLimit": "已超过文件大小限制",
"errorAcceptingAccessRequest": "接受访问请求时发生错误",
"errorDecliningAccessRequest": "拒绝访问请求时发生错误",
"errorGettingAccessRequests": "获取访问请求时发生错误"
},
"actions": {
"undo": "撤销",
Expand Down
Loading
Loading