Skip to content

Commit c4820cb

Browse files
authored
Fix/app packaging (#388)
1 parent 3d824a1 commit c4820cb

7 files changed

Lines changed: 129 additions & 42 deletions

File tree

.erb/configs/webpack.config.base.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
import webpack from 'webpack';
66
import webpackPaths from './webpack.paths';
77

8-
// Only packages that were in release/app/package.json should be external
9-
// These are native modules or runtime dependencies that cannot/should not be bundled
10-
const nativeExternals = [
11-
'better-sqlite3',
12-
'reflect-metadata',
13-
'typeorm',
14-
];
8+
// Keep JS deps bundled, but leave native modules as externals.
9+
// better-sqlite3 relies on runtime native loading (bindings), which breaks when bundled.
10+
const nativeExternals = ['better-sqlite3'];
1511

1612
const configuration: webpack.Configuration = {
1713
externals: nativeExternals,

package.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
"appId": "com.internxt.drive",
4949
"asar": true,
5050
"asarUnpack": [
51-
"**\\*.{node,dll}",
51+
"**/*.{node,dll}",
52+
"**/node_modules/better-sqlite3/**",
53+
"**/node_modules/bindings/**",
5254
"**/node_modules/sharp/**"
5355
],
5456
"files": [
@@ -90,7 +92,28 @@
9092
"./assets/**",
9193
"./clamAV/**",
9294
"./src/apps/nautilus-extension/internxt-virtual-drive.py",
93-
"./dist/fuse-daemon"
95+
"./dist/fuse-daemon",
96+
{
97+
"from": "./node_modules/better-sqlite3",
98+
"to": "node_modules/better-sqlite3",
99+
"filter": [
100+
"**/*"
101+
]
102+
},
103+
{
104+
"from": "./node_modules/bindings",
105+
"to": "node_modules/bindings",
106+
"filter": [
107+
"**/*"
108+
]
109+
},
110+
{
111+
"from": "./node_modules/file-uri-to-path",
112+
"to": "node_modules/file-uri-to-path",
113+
"filter": [
114+
"**/*"
115+
]
116+
}
94117
]
95118
},
96119
"devDependencies": {

src/apps/drive/hydration-api/HydrationApi.test.ts

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as filesRoute from './routes/files';
55
import { partialSpyOn } from 'tests/vitest/utils.helper';
66
import express from 'express';
77
import { loggerMock } from 'tests/vitest/mocks.helper';
8+
import { Server } from 'http';
89

910
const buildHydrationRouterMock = partialSpyOn(contentsRoute, 'buildHydrationRouter');
1011
const buildFilesRouterMock = partialSpyOn(filesRoute, 'buildFilesRouter');
@@ -18,46 +19,100 @@ function createMockContainer() {
1819
describe('HydrationApi', () => {
1920
let container: Container;
2021
let hydrationApi: HydrationApi;
22+
let connectionHandler: ((socket: import('node:net').Socket) => void) | null;
23+
24+
function mockServerListen(api: HydrationApi) {
25+
const closeMock = vi.fn((callback?: (error?: Error) => void) => {
26+
callback?.();
27+
});
28+
29+
const server = {
30+
on: vi.fn((event: string, handler: (socket: import('node:net').Socket) => void) => {
31+
if (event === 'connection') {
32+
connectionHandler = handler;
33+
}
34+
35+
return server;
36+
}),
37+
close: closeMock,
38+
} as unknown as Server;
39+
40+
const app = (api as unknown as { app: express.Express }).app;
41+
vi.spyOn(app, 'listen').mockImplementation((_port: number, callback?: () => void) => {
42+
callback?.();
43+
return server;
44+
});
45+
46+
return { closeMock };
47+
}
2148

2249
beforeEach(() => {
2350
container = createMockContainer();
51+
connectionHandler = null;
2452

2553
buildHydrationRouterMock.mockReturnValue(express.Router());
2654
buildFilesRouterMock.mockReturnValue(express.Router());
2755
});
2856

2957
afterEach(async () => {
30-
await hydrationApi.stop();
58+
if (hydrationApi) {
59+
await hydrationApi.stop();
60+
}
3161
});
3262

3363
describe('start', () => {
3464
it('should start the server and log the port', async () => {
3565
hydrationApi = new HydrationApi(container);
66+
mockServerListen(hydrationApi);
3667

3768
await hydrationApi.start({ debug: false, timeElapsed: false });
3869

39-
expect(loggerMock.debug).toBeCalledWith({
70+
expect(loggerMock.debug).toHaveBeenCalledWith({
4071
msg: '[HYDRATION API] running on port 4567',
4172
});
4273
});
4374

4475
it('should build hydration and files routers with the container', async () => {
4576
hydrationApi = new HydrationApi(container);
77+
mockServerListen(hydrationApi);
4678

4779
await hydrationApi.start({ debug: false, timeElapsed: false });
4880

49-
expect(buildHydrationRouterMock).toBeCalledWith(container);
50-
expect(buildFilesRouterMock).toBeCalledWith(container);
81+
expect(buildHydrationRouterMock).toHaveBeenCalledWith(container);
82+
expect(buildFilesRouterMock).toHaveBeenCalledWith(container);
5183
});
5284

5385
it('should enable debug logging middleware when debug is true', async () => {
5486
hydrationApi = new HydrationApi(container);
87+
mockServerListen(hydrationApi);
88+
89+
const app = (hydrationApi as unknown as { app: express.Express }).app;
90+
const appUseMock = vi.spyOn(app, 'use');
5591

5692
await hydrationApi.start({ debug: true, timeElapsed: false });
5793

58-
await fetch('http://localhost:4567/hydration/test');
59-
// The request itself may 404, but the debug middleware should have logged
60-
expect(loggerMock.debug).toBeCalledWith(
94+
const middlewareCandidates = appUseMock.mock.calls.flatMap((args) => args as unknown[]);
95+
const debugMiddlewareCandidate = middlewareCandidates.find(
96+
(arg) =>
97+
typeof arg === 'function' &&
98+
(arg as (req: express.Request, res: express.Response, next: express.NextFunction) => void).length === 3,
99+
);
100+
const debugMiddleware = debugMiddlewareCandidate as express.RequestHandler | undefined;
101+
102+
expect(debugMiddleware).toBeDefined();
103+
104+
const next = vi.fn();
105+
debugMiddleware?.(
106+
{
107+
method: 'GET',
108+
url: '/hydration/test',
109+
} as express.Request,
110+
{} as express.Response,
111+
next,
112+
);
113+
114+
expect(next).toHaveBeenCalledTimes(1);
115+
expect(loggerMock.debug).toHaveBeenCalledWith(
61116
expect.objectContaining({
62117
msg: expect.stringContaining('[HYDRATION API]'),
63118
}),
@@ -74,34 +129,37 @@ describe('HydrationApi', () => {
74129

75130
it('should stop the server after it was started', async () => {
76131
hydrationApi = new HydrationApi(container);
132+
mockServerListen(hydrationApi);
77133
await hydrationApi.start({ debug: false, timeElapsed: false });
78134

79135
await expect(hydrationApi.stop()).resolves.toBeUndefined();
80136
});
81137

82138
it('should destroy open sockets when stopping', async () => {
83139
hydrationApi = new HydrationApi(container);
140+
mockServerListen(hydrationApi);
84141
await hydrationApi.start({ debug: false, timeElapsed: false });
85142

86-
// Create a connection to generate an open socket
87-
const socket = new (await import('node:net')).Socket();
88-
await new Promise<void>((resolve, reject) => {
89-
socket.connect(4567, '127.0.0.1', () => resolve());
90-
socket.on('error', reject);
91-
});
143+
const socket = {
144+
destroy: vi.fn(),
145+
once: vi.fn(),
146+
} as unknown as import('node:net').Socket;
92147

93-
// Small delay to ensure the server registers the socket
94-
await new Promise((resolve) => setTimeout(resolve, 50));
148+
expect(connectionHandler).toBeDefined();
149+
connectionHandler?.(socket);
95150

96151
await expect(hydrationApi.stop()).resolves.toBeUndefined();
152+
expect(socket.destroy).toHaveBeenCalledTimes(1);
97153
});
98154

99155
it('should be safe to call stop multiple times', async () => {
100156
hydrationApi = new HydrationApi(container);
157+
const { closeMock } = mockServerListen(hydrationApi);
101158
await hydrationApi.start({ debug: false, timeElapsed: false });
102159

103160
await hydrationApi.stop();
104161
await expect(hydrationApi.stop()).resolves.toBeUndefined();
162+
expect(closeMock).toHaveBeenCalledTimes(1);
105163
});
106164
});
107165
});

src/apps/main/auth/get-credentials.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { safeStorage } from 'electron';
2-
import { logger } from '@internxt/drive-desktop-core/build/backend';
32
import { getCredentials } from './get-credentials';
43
import ConfigStore, { AppStore } from '../config';
54
import { calls, call, partialSpyOn } from 'tests/vitest/utils.helper';
5+
import { loggerMock } from 'tests/vitest/mocks.helper';
66

77
describe('getCredentials', () => {
88
const configGetMock = partialSpyOn(ConfigStore, 'get');
99
const safeStorageIsAvailableMock = partialSpyOn(safeStorage, 'isEncryptionAvailable');
1010
const safeStorageDecryptMock = partialSpyOn(safeStorage, 'decryptString');
11-
const loggerErrorMock = partialSpyOn(logger, 'error');
1211

1312
const plainToken = 'plain-token-123';
1413
const plainMnemonic = 'plain mnemonic words here';
@@ -120,7 +119,7 @@ describe('getCredentials', () => {
120119
});
121120

122121
describe('when safeStorage is not available', () => {
123-
it('should return plaintext values and log error', () => {
122+
it('should return empty values and log warning', () => {
124123
configGetMock.mockImplementation((key: keyof AppStore) => {
125124
const values: Record<string, unknown> = {
126125
newToken: encryptedToken,
@@ -136,19 +135,19 @@ describe('getCredentials', () => {
136135
const result = getCredentials();
137136

138137
expect(result).toMatchObject({
139-
newToken: encryptedToken,
140-
mnemonic: encryptedMnemonic,
138+
newToken: '',
139+
mnemonic: '',
141140
});
142-
call(loggerErrorMock).toMatchObject({
143-
msg: '[AUTH] Safe Storage was not available when decrypting encrypted token',
141+
call(loggerMock.warn).toMatchObject({
142+
msg: '[AUTH] Safe Storage was not available when decrypting encrypted token, falling back to logged-out state',
144143
tag: 'AUTH',
145144
});
146145
calls(safeStorageDecryptMock).toHaveLength(0);
147146
});
148147
});
149148

150149
describe('when decryption fails', () => {
151-
it('should throw error and log it', () => {
150+
it('should return empty values and log warning', () => {
152151
configGetMock.mockImplementation((key: keyof AppStore) => {
153152
const values: Record<string, unknown> = {
154153
newToken: encryptedToken,
@@ -165,9 +164,14 @@ describe('getCredentials', () => {
165164
throw decryptError;
166165
});
167166

168-
expect(() => getCredentials()).toThrow();
169-
call(loggerErrorMock).toMatchObject({
170-
msg: '[AUTH] Failed to decrypt token',
167+
const result = getCredentials();
168+
169+
expect(result).toMatchObject({
170+
newToken: '',
171+
mnemonic: '',
172+
});
173+
call(loggerMock.debug).toMatchObject({
174+
msg: '[AUTH] Failed to decrypt token, falling back to logged-out state',
171175
tag: 'AUTH',
172176
error: decryptError,
173177
});

src/apps/main/auth/get-credentials.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import ConfigStore from '../config';
44

55
const TOKEN_ENCODING = 'latin1';
66

7+
function emptyCredentials() {
8+
return { newToken: '', mnemonic: '' };
9+
}
10+
711
export function getCredentials() {
812
const newToken = ConfigStore.get('newToken');
913
const mnemonic = ConfigStore.get('mnemonic');
@@ -16,11 +20,11 @@ export function getCredentials() {
1620

1721
try {
1822
if (!safeStorage.isEncryptionAvailable()) {
19-
logger.error({
20-
msg: '[AUTH] Safe Storage was not available when decrypting encrypted token',
23+
logger.warn({
24+
msg: '[AUTH] Safe Storage was not available when decrypting encrypted token, falling back to logged-out state',
2125
tag: 'AUTH',
2226
});
23-
return { newToken, mnemonic };
27+
return emptyCredentials();
2428
}
2529

2630
const decryptedToken = tokenEncrypted ? safeStorage.decryptString(Buffer.from(newToken, TOKEN_ENCODING)) : newToken;
@@ -34,10 +38,12 @@ export function getCredentials() {
3438
mnemonic: decryptedMnemonic,
3539
};
3640
} catch (err) {
37-
throw logger.error({
38-
msg: '[AUTH] Failed to decrypt token',
41+
logger.debug({
42+
msg: '[AUTH] Failed to decrypt token, falling back to logged-out state',
3943
tag: 'AUTH',
4044
error: err,
4145
});
46+
47+
return emptyCredentials();
4248
}
4349
}

src/apps/main/database/collections/DriveFileCollection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
55
import { logger } from '@internxt/drive-desktop-core/build/backend';
66

77
export class DriveFilesCollection implements DatabaseCollectionAdapter<DriveFile> {
8-
private repository: Repository<DriveFile> = AppDataSource.getRepository('drive_file');
8+
private repository: Repository<DriveFile> = AppDataSource.getRepository(DriveFile);
99

1010
async connect(): Promise<{ success: boolean }> {
1111
return {

src/apps/main/database/collections/DriveFolderCollection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
55
import { logger } from '@internxt/drive-desktop-core/build/backend';
66

77
export class DriveFoldersCollection implements DatabaseCollectionAdapter<DriveFolder> {
8-
private repository: Repository<DriveFolder> = AppDataSource.getRepository('drive_folder');
8+
private repository: Repository<DriveFolder> = AppDataSource.getRepository(DriveFolder);
99
async connect(): Promise<{ success: boolean }> {
1010
return {
1111
success: true,

0 commit comments

Comments
 (0)