Skip to content

Commit 60e1585

Browse files
committed
feat(nautilus-extension): implement sharing link generation and domain fetching
1 parent 373baff commit 60e1585

30 files changed

Lines changed: 899 additions & 94 deletions

scripts/tsc-max-errors.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Maximum allowed TypeScript errors (current baseline)
44
# TODO: Decrease this number as errors are fixed, never increase it
55
# Note: CI environment may have different error count than local due to dependency differences
6-
MAX_ERRORS=150
6+
MAX_ERRORS=146
77

88
# Run TypeScript compiler and capture output
99
OUTPUT=$(tsc --noEmit --pretty --skipLibCheck 2>&1)

src/__mocks__/electron.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,14 @@ const clipboard = {
5555
writeText: vi.fn(),
5656
};
5757

58-
export { app, ipcMain, ipcRenderer, dialog, BrowserWindow, safeStorage, nativeImage, shell, clipboard };
58+
class Notification {
59+
static show = vi.fn();
60+
61+
constructor(_options: { title: string; body: string }) {}
62+
63+
show() {
64+
Notification.show();
65+
}
66+
}
67+
68+
export { app, ipcMain, ipcRenderer, dialog, BrowserWindow, safeStorage, nativeImage, shell, clipboard, Notification };
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { Container } from 'diod';
2+
import { NextFunction, Request, Response } from 'express';
3+
import { mockDeep } from 'vitest-mock-extended';
4+
import { call } from 'tests/vitest/utils.helper';
5+
import { buildContentsController } from './contents';
6+
import { StorageFileDeleter } from '../../../../context/storage/StorageFiles/application/delete/StorageFileDeleter';
7+
import { AllFilesInFolderAreAvailableOffline } from '../../../../context/storage/StorageFolders/application/offline/AllFilesInFolderAreAvailableOffline';
8+
import { MakeStorageFileAvaliableOffline } from '../../../../context/storage/StorageFiles/application/offline/MakeStorageFileAvaliableOffline';
9+
import { StorageFileIsAvailableOffline } from '../../../../context/storage/StorageFiles/application/offline/StorageFileIsAvailableOffline';
10+
import { MakeFolderAvaliableOffline } from '../../../../context/storage/StorageFolders/application/offline/MakeFolderAvaliableOffline';
11+
import { StorageFolderDeleter } from '../../../../context/storage/StorageFolders/application/delete/StorageFolderDeleter';
12+
import * as generateLinkModule from '../../../../backend/features/nautilus-extension/create-sharing-link/generate-link';
13+
14+
describe('contents', () => {
15+
const generateLinkMock = vi.spyOn(generateLinkModule, 'generateLink');
16+
17+
let req: ReturnType<typeof mockDeep<Request>>;
18+
let res: ReturnType<typeof mockDeep<Response>>;
19+
let next: NextFunction;
20+
21+
const storageFileIsAvailableRun = vi.fn();
22+
const allFilesInFolderAreAvailableRun = vi.fn();
23+
const storageFileDeleterRun = vi.fn();
24+
const makeStorageFileAvaliableOfflineRun = vi.fn();
25+
const makeFolderAvaliableOfflineRun = vi.fn();
26+
const storageFolderDeleterRun = vi.fn();
27+
28+
function createContainer() {
29+
const services = new Map<unknown, { run: (...args: unknown[]) => unknown }>([
30+
[StorageFileIsAvailableOffline, { run: storageFileIsAvailableRun }],
31+
[AllFilesInFolderAreAvailableOffline, { run: allFilesInFolderAreAvailableRun }],
32+
[StorageFileDeleter, { run: storageFileDeleterRun }],
33+
[MakeStorageFileAvaliableOffline, { run: makeStorageFileAvaliableOfflineRun }],
34+
[MakeFolderAvaliableOffline, { run: makeFolderAvaliableOfflineRun }],
35+
[StorageFolderDeleter, { run: storageFolderDeleterRun }],
36+
]);
37+
38+
return {
39+
get: vi.fn((token: unknown) => {
40+
const service = services.get(token);
41+
42+
if (!service) throw new Error('Service not mocked');
43+
44+
return service;
45+
}),
46+
} as unknown as Container;
47+
}
48+
49+
function encodePath(path: string) {
50+
return Buffer.from(path, 'utf-8').toString('base64');
51+
}
52+
53+
beforeEach(() => {
54+
req = mockDeep<Request>();
55+
res = mockDeep<Response>();
56+
next = vi.fn((error?: unknown) => error) as unknown as NextFunction;
57+
58+
req.params = { path: encodePath('/folder/file.txt') };
59+
60+
res.status.mockReturnValue(res);
61+
res.json.mockReturnValue(res);
62+
63+
storageFileIsAvailableRun.mockResolvedValue(false);
64+
allFilesInFolderAreAvailableRun.mockResolvedValue(false);
65+
storageFileDeleterRun.mockResolvedValue(undefined);
66+
makeStorageFileAvaliableOfflineRun.mockResolvedValue(undefined);
67+
makeFolderAvaliableOfflineRun.mockResolvedValue(undefined);
68+
storageFolderDeleterRun.mockResolvedValue(undefined);
69+
generateLinkMock.mockResolvedValue('https://link.test');
70+
});
71+
72+
it('should return generated link in copyLink', async () => {
73+
req.params.path = encodePath('/folder/my%20file.txt');
74+
75+
const controller = buildContentsController(createContainer());
76+
77+
await controller.copyLink(req, res, next);
78+
79+
call(generateLinkMock).toStrictEqual({ path: '/folder/my file.txt' });
80+
call(res.status).toBe(202);
81+
call(res.json).toStrictEqual({ path: '/folder/my file.txt', link: 'https://link.test' });
82+
});
83+
84+
it('should call next when copyLink fails', async () => {
85+
const error = new Error('copy failed');
86+
generateLinkMock.mockRejectedValue(error);
87+
88+
const controller = buildContentsController(createContainer());
89+
90+
await controller.copyLink(req, res, next);
91+
92+
call(next).toBe(error);
93+
});
94+
95+
it('should resolve file availability in get', async () => {
96+
storageFileIsAvailableRun.mockResolvedValue(true);
97+
98+
const controller = buildContentsController(createContainer());
99+
100+
await controller.get(req, res);
101+
102+
call(res.json).toStrictEqual({ locallyAvaliable: true });
103+
});
104+
105+
it('should fallback to folder availability in get when file lookup fails', async () => {
106+
storageFileIsAvailableRun.mockRejectedValue(new Error('not a file'));
107+
allFilesInFolderAreAvailableRun.mockResolvedValue(true);
108+
109+
const controller = buildContentsController(createContainer());
110+
111+
await controller.get(req, res);
112+
113+
call(res.json).toStrictEqual({ locallyAvaliable: true });
114+
});
115+
116+
it('should return false in getFile when file lookup fails', async () => {
117+
storageFileIsAvailableRun.mockRejectedValue(new Error('not found'));
118+
119+
const controller = buildContentsController(createContainer());
120+
121+
await controller.getFile(req, res);
122+
123+
call(res.json).toStrictEqual({ locallyAvaliable: false });
124+
});
125+
126+
it('should return false in getFolder when folder lookup fails', async () => {
127+
allFilesInFolderAreAvailableRun.mockRejectedValue(new Error('not found'));
128+
129+
const controller = buildContentsController(createContainer());
130+
131+
await controller.getFolder(req, res);
132+
133+
call(res.json).toStrictEqual({ locallyAvaliable: false });
134+
});
135+
136+
it('should remove file and return 201', async () => {
137+
req.params.path = encodePath('/folder/to-delete.txt');
138+
139+
const controller = buildContentsController(createContainer());
140+
141+
await controller.removeFile(req, res);
142+
143+
call(storageFileDeleterRun).toBe('/folder/to-delete.txt');
144+
call(res.status).toBe(201);
145+
expect(res.send).toHaveBeenCalled();
146+
});
147+
148+
it('should download file and return 202', async () => {
149+
req.params.path = encodePath('/folder/to-download.txt');
150+
151+
const controller = buildContentsController(createContainer());
152+
153+
await controller.downloadFile(req, res, next);
154+
155+
call(makeStorageFileAvaliableOfflineRun).toBe('/folder/to-download.txt');
156+
call(res.status).toBe(202);
157+
expect(res.send).toHaveBeenCalled();
158+
});
159+
160+
it('should call next when downloadFile fails', async () => {
161+
const error = new Error('download file failed');
162+
163+
const container = {
164+
get: vi.fn(() => {
165+
throw error;
166+
}),
167+
} as unknown as Container;
168+
169+
const controller = buildContentsController(container);
170+
171+
await controller.downloadFile(req, res, next);
172+
173+
call(next).toBe(error);
174+
});
175+
176+
it('should remove folder and return 201', async () => {
177+
req.params.path = encodePath('/folder/to-delete');
178+
179+
const controller = buildContentsController(createContainer());
180+
181+
await controller.removeFolder(req, res);
182+
183+
call(storageFolderDeleterRun).toBe('/folder/to-delete');
184+
call(res.status).toBe(201);
185+
expect(res.send).toHaveBeenCalled();
186+
});
187+
188+
it('should download folder and return 202', async () => {
189+
req.params.path = encodePath('/folder/to-download');
190+
191+
const controller = buildContentsController(createContainer());
192+
193+
await controller.downloadFolder(req, res, next);
194+
195+
call(makeFolderAvaliableOfflineRun).toBe('/folder/to-download');
196+
call(res.status).toBe(202);
197+
expect(res.send).toHaveBeenCalled();
198+
});
199+
200+
it('should call next when downloadFolder fails', async () => {
201+
const error = new Error('download folder failed');
202+
203+
const container = {
204+
get: vi.fn(() => {
205+
throw error;
206+
}),
207+
} as unknown as Container;
208+
209+
const controller = buildContentsController(container);
210+
211+
await controller.downloadFolder(req, res, next);
212+
213+
call(next).toBe(error);
214+
});
215+
});

src/apps/drive/hydration-api/controllers/contents.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { StorageFileIsAvailableOffline } from '../../../../context/storage/Stora
88
import { Optional } from '../../../../shared/types/Optional';
99
import { MakeFolderAvaliableOffline } from '../../../../context/storage/StorageFolders/application/offline/MakeFolderAvaliableOffline';
1010
import { StorageFolderDeleter } from '../../../../context/storage/StorageFolders/application/delete/StorageFolderDeleter';
11-
import { generateLink } from '../../../main/nautilus-extension/create-sharing-link/generate-link';
11+
import { generateLink } from '../../../../backend/features/nautilus-extension/create-sharing-link/generate-link';
1212

1313
export function buildContentsController(container: Container) {
1414
async function isFileLocallyAvailable(path: string): Promise<Optional<boolean>> {
@@ -142,7 +142,7 @@ export function buildContentsController(container: Container) {
142142
}
143143
};
144144

145-
const copyLink = async (req: Request, res: Response, next: NextFunction) => {
145+
async function copyLink(req: Request, res: Response, next: NextFunction) {
146146
try {
147147
const decodedBuffer = Buffer.from(req.params.path, 'base64');
148148

@@ -155,7 +155,7 @@ export function buildContentsController(container: Container) {
155155
next(error);
156156
return;
157157
}
158-
};
158+
}
159159

160160
return {
161161
copyLink,

src/apps/main/nautilus-extension/create-sharing-link/fetch-random-domain.ts

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/apps/main/nautilus-extension/create-sharing-link/to-error.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { DriveServerError } from '../../../../infra/drive-server/drive-server.error';
2+
import * as createSharingModule from '../../../../infra/drive-server/services/sharings/services/create-sharing';
3+
import { createSharingResult } from './create-sharing-result';
4+
import { call, partialSpyOn } from 'tests/vitest/utils.helper';
5+
6+
describe('create-sharing-result', () => {
7+
const createSharingMock = partialSpyOn(createSharingModule, 'createSharing');
8+
9+
beforeEach(() => {
10+
createSharingMock.mockResolvedValue({
11+
data: {
12+
encryptedCode: 'server-encrypted-code',
13+
id: 'sharing-id',
14+
},
15+
} as object);
16+
});
17+
18+
it('should call createSharing with expected payload and return sharing data', async () => {
19+
const result = await createSharingResult({
20+
encryptedCode: 'local-encrypted-code',
21+
encryptionKey: 'encrypted-mnemonic',
22+
item: {
23+
itemId: 'file-uuid',
24+
itemType: 'file',
25+
},
26+
});
27+
28+
call(createSharingMock).toStrictEqual({
29+
body: {
30+
encryptedCode: 'local-encrypted-code',
31+
encryptedPassword: null,
32+
encryptionAlgorithm: 'inxt-v2',
33+
encryptionKey: 'encrypted-mnemonic',
34+
itemId: 'file-uuid',
35+
itemType: 'file',
36+
persistPreviousSharing: true,
37+
},
38+
});
39+
40+
expect(result).toStrictEqual({
41+
encryptedCode: 'server-encrypted-code',
42+
id: 'sharing-id',
43+
});
44+
});
45+
46+
it('should throw transformed error when createSharing returns error', async () => {
47+
createSharingMock.mockResolvedValue({ error: new DriveServerError('NOT_FOUND', 404, 'not found') } as object);
48+
49+
await expect(
50+
createSharingResult({
51+
encryptedCode: 'local-encrypted-code',
52+
encryptionKey: 'encrypted-mnemonic',
53+
item: {
54+
itemId: 'folder-uuid',
55+
itemType: 'folder',
56+
},
57+
}),
58+
).rejects.toThrow('Error while creating sharing: not found');
59+
});
60+
});

0 commit comments

Comments
 (0)