Skip to content

Commit 241593d

Browse files
d-gubertgabriellsh
authored andcommitted
fix(apps): prevent installation invalidation on app cron updates (#37152)
1 parent 8491125 commit 241593d

File tree

9 files changed

+286
-29
lines changed

9 files changed

+286
-29
lines changed

.changeset/rotten-jars-occur.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/apps-engine': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Fixes a bug that would cause apps to go into `invalid_installation_disabled` in some cases

packages/apps-engine/src/server/AppManager.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,15 @@ export class AppManager {
909909
}
910910

911911
appStorageItem.marketplaceInfo[0].subscriptionInfo = appInfo.subscriptionInfo;
912+
appStorageItem.signature = await this.getSignatureManager().signApp(appStorageItem);
912913

913-
return this.appMetadataStorage.updateMarketplaceInfo(appStorageItem._id, appStorageItem.marketplaceInfo);
914+
return this.appMetadataStorage.updatePartialAndReturnDocument({
915+
_id: appStorageItem._id,
916+
marketplaceInfo: appStorageItem.marketplaceInfo,
917+
signature: appStorageItem.signature,
918+
});
914919
}),
915-
).catch();
920+
).catch(() => {});
916921

917922
const queue = [] as Array<Promise<void>>;
918923

@@ -933,7 +938,7 @@ export class AppManager {
933938
return;
934939
}
935940

936-
await this.purgeAppConfig(app);
941+
await this.purgeAppConfig(app, { keepScheduledJobs: true });
937942

938943
return app.setStatus(AppStatus.INVALID_LICENSE_DISABLED);
939944
})

packages/apps-engine/tests/server/AppManager.spec.ts

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Expect, SetupFixture, Teardown, Test } from 'alsatian';
1+
import { AsyncTest, Expect, SetupFixture, SpyOn, Teardown, Test } from 'alsatian';
22

33
import { AppManager } from '../../src/server/AppManager';
44
import { AppBridges } from '../../src/server/bridges';
@@ -14,7 +14,7 @@ import {
1414
AppOutboundCommunicationProviderManager,
1515
} from '../../src/server/managers';
1616
import type { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage';
17-
import { SimpleClass, TestInfastructureSetup } from '../test-data/utilities';
17+
import { SimpleClass, TestData, TestInfastructureSetup } from '../test-data/utilities';
1818

1919
export class AppManagerTestFixture {
2020
private testingInfastructure: TestInfastructureSetup;
@@ -121,4 +121,142 @@ export class AppManagerTestFixture {
121121
Expect(manager.getVideoConfProviderManager() instanceof AppVideoConfProviderManager).toBe(true);
122122
Expect(manager.getOutboundCommunicationProviderManager() instanceof AppOutboundCommunicationProviderManager).toBe(true);
123123
}
124+
125+
@AsyncTest('Update Apps Marketplace Info - Apps without subscription info are skipped')
126+
public async updateAppsMarketplaceInfoSkipsAppsWithoutSubscriptionInfo() {
127+
const manager = new AppManager({
128+
metadataStorage: this.testingInfastructure.getAppStorage(),
129+
logStorage: this.testingInfastructure.getLogStorage(),
130+
bridges: this.testingInfastructure.getAppBridges(),
131+
sourceStorage: this.testingInfastructure.getSourceStorage(),
132+
});
133+
134+
const appsOverview = TestData.getAppsOverview();
135+
appsOverview[0].latest.subscriptionInfo = undefined; // No subscription info
136+
137+
// Mock the apps Map to return our mock app
138+
(manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]);
139+
140+
const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument');
141+
updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve());
142+
143+
// Should not throw and complete successfully
144+
await manager.updateAppsMarketplaceInfo(appsOverview);
145+
146+
Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled();
147+
}
148+
149+
@AsyncTest('Update Apps Marketplace Info - Apps not found in manager are skipped')
150+
public async updateAppsMarketplaceInfoSkipsAppsNotInManager() {
151+
const manager = new AppManager({
152+
metadataStorage: this.testingInfastructure.getAppStorage(),
153+
logStorage: this.testingInfastructure.getLogStorage(),
154+
bridges: this.testingInfastructure.getAppBridges(),
155+
sourceStorage: this.testingInfastructure.getSourceStorage(),
156+
});
157+
158+
const appsOverview = TestData.getAppsOverview();
159+
appsOverview[0].latest.id = 'nonexistent-app'; // App not in manager
160+
161+
// Mock the apps Map to return our mock app
162+
(manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]);
163+
164+
const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument');
165+
updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve());
166+
167+
// Should not throw and complete successfully
168+
await manager.updateAppsMarketplaceInfo(appsOverview);
169+
170+
Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled();
171+
}
172+
173+
@AsyncTest('Update Apps Marketplace Info - Apps with same license are skipped')
174+
public async updateAppsMarketplaceInfoSkipsAppsWithSameLicense() {
175+
const manager = new AppManager({
176+
metadataStorage: this.testingInfastructure.getAppStorage(),
177+
logStorage: this.testingInfastructure.getLogStorage(),
178+
bridges: this.testingInfastructure.getAppBridges(),
179+
sourceStorage: this.testingInfastructure.getSourceStorage(),
180+
});
181+
182+
const sameLicenseData = 'same-license-data';
183+
const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
184+
license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') },
185+
});
186+
187+
const mockStorageItem = TestData.getAppStorageItem({
188+
marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })],
189+
});
190+
191+
const mockApp = TestData.getMockApp(mockStorageItem, manager);
192+
193+
// Mock the apps Map to return our mock app
194+
(manager as any).apps = new Map([['test-app', mockApp]]);
195+
196+
const appsOverview = TestData.getAppsOverview(
197+
TestData.getMarketplaceSubscriptionInfo({
198+
license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') },
199+
}),
200+
);
201+
202+
const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument');
203+
updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve());
204+
205+
// Should not throw and complete successfully
206+
await manager.updateAppsMarketplaceInfo(appsOverview);
207+
208+
// Verify the subscription info was not updated (should remain the same)
209+
Expect(mockStorageItem.marketplaceInfo[0].subscriptionInfo.seats).toBe(10); // Original value
210+
Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled();
211+
}
212+
213+
@AsyncTest('Update Apps Marketplace Info - Subscription info is updated and app is signed')
214+
public async updateAppsMarketplaceInfoUpdatesSubscriptionAndSignsApp() {
215+
const manager = new AppManager({
216+
metadataStorage: this.testingInfastructure.getAppStorage(),
217+
logStorage: this.testingInfastructure.getLogStorage(),
218+
bridges: this.testingInfastructure.getAppBridges(),
219+
sourceStorage: this.testingInfastructure.getSourceStorage(),
220+
});
221+
222+
const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
223+
license: { license: 'old-license-data', version: 1, expireDate: new Date('2023-01-01') },
224+
});
225+
226+
const newSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({
227+
seats: 20,
228+
maxSeats: 200,
229+
startDate: '2023-02-01',
230+
periodEnd: '2024-01-31',
231+
license: { license: 'new-license-data', version: 1, expireDate: new Date('2026-01-01') },
232+
});
233+
234+
const mockStorageItem = TestData.getAppStorageItem({
235+
marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })],
236+
});
237+
238+
const mockApp = TestData.getMockApp(mockStorageItem, manager);
239+
240+
// eslint-disable-next-line no-return-assign
241+
SpyOn(manager.getSignatureManager(), 'signApp').andReturn(Promise.resolve('signed-app-data'));
242+
SpyOn(mockApp, 'validateLicense').andReturn(Promise.resolve());
243+
244+
const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument');
245+
updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve(mockStorageItem));
246+
247+
// Mock the apps Map and dependencies
248+
(manager as any).apps = new Map([['test-app', mockApp]]);
249+
250+
const appsOverview = TestData.getAppsOverview(newSubscriptionInfo);
251+
252+
await manager.updateAppsMarketplaceInfo(appsOverview);
253+
254+
const expectedStorageItem = mockApp.getStorageItem();
255+
256+
// Verify the subscription info was updated
257+
Expect(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.seats).toBe(20);
258+
Expect(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.license.license).toBe('new-license-data');
259+
Expect(expectedStorageItem.signature).toBe('signed-app-data');
260+
Expect(updatePartialAndReturnDocumentSpy).toHaveBeenCalled().exactly(1).times;
261+
}
124262
}

packages/apps-engine/tests/server/managers/AppApiManager.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
import { AppAccessorManager, AppApiManager } from '../../../src/server/managers';
1717
import { AppApi } from '../../../src/server/managers/AppApi';
1818
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
19-
import type { AppLogStorage } from '../../../src/server/storage';
19+
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
2020
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
2121
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
2222
import { TestData } from '../../test-data/utilities';
@@ -38,7 +38,7 @@ export class AppApiManagerTestFixture {
3838
public setupFixture() {
3939
this.mockBridges = new TestsAppBridges();
4040

41-
this.mockApp = TestData.getMockApp({ id: 'testing', name: 'TestApp' }, this.mockManager);
41+
this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, this.mockManager);
4242

4343
const bri = this.mockBridges;
4444
const app = this.mockApp;

packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Expect, SetupFixture, Test } from 'alsatian';
22

3+
import type { IAppStorageItem } from '../../../server/storage';
34
import type { ISlashCommand } from '../../../src/definition/slashcommands';
45
import type { AppManager } from '../../../src/server/AppManager';
56
import type { ProxiedApp } from '../../../src/server/ProxiedApp';
@@ -11,7 +12,7 @@ export class AppSlashCommandRegistrationTestFixture {
1112

1213
@SetupFixture
1314
public setupFixture() {
14-
this.mockApp = TestData.getMockApp({ id: 'test', name: 'TestApp' }, {} as AppManager);
15+
this.mockApp = TestData.getMockApp({ info: { id: 'test', name: 'TestApp' } } as IAppStorageItem, {} as AppManager);
1516
}
1617

1718
@Test()

packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { AppAccessorManager, AppSlashCommandManager } from '../../../src/server/
1717
import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand';
1818
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
1919
import { Room } from '../../../src/server/rooms/Room';
20-
import type { AppLogStorage } from '../../../src/server/storage';
20+
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
2121
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
2222
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
2323
import { TestData } from '../../test-data/utilities';
@@ -39,7 +39,7 @@ export class AppSlashCommandManagerTestFixture {
3939
public setupFixture() {
4040
this.mockBridges = new TestsAppBridges();
4141

42-
this.mockApp = TestData.getMockApp({ id: 'testing', name: 'TestApp' }, this.mockManager);
42+
this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, this.mockManager);
4343

4444
const bri = this.mockBridges;
4545
const app = this.mockApp;

packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { AppApiManager, AppExternalComponentManager, AppSchedulerManager, A
88
import { AppAccessorManager, AppVideoConfProviderManager } from '../../../src/server/managers';
99
import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider';
1010
import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager';
11-
import type { AppLogStorage } from '../../../src/server/storage';
11+
import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage';
1212
import { TestsAppBridges } from '../../test-data/bridges/appBridges';
1313
import { TestsAppLogStorage } from '../../test-data/storage/logStorage';
1414
import { TestData } from '../../test-data/utilities';
@@ -28,7 +28,7 @@ export class AppVideoConfProviderManagerTestFixture {
2828
public setupFixture() {
2929
this.mockBridges = new TestsAppBridges();
3030

31-
this.mockApp = TestData.getMockApp({ id: 'testing', name: 'testing' }, this.mockManager);
31+
this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'testing' } } as IAppStorageItem, this.mockManager);
3232

3333
const bri = this.mockBridges;
3434
const app = this.mockApp;

packages/apps-engine/tests/test-data/storage/storage.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import type { AppStatus } from '../../../src/definition/AppStatus';
2+
import type { IAppInfo } from '../../../src/definition/metadata';
3+
import type { ISetting } from '../../../src/definition/settings';
4+
import type { IMarketplaceInfo } from '../../../src/server/marketplace';
15
import type { IAppStorageItem } from '../../../src/server/storage';
26
import { AppMetadataStorage } from '../../../src/server/storage';
37

@@ -82,20 +86,6 @@ export class TestsAppStorage extends AppMetadataStorage {
8286
});
8387
}
8488

85-
public update(item: IAppStorageItem): Promise<IAppStorageItem> {
86-
return new Promise((resolve, reject) => {
87-
this.db.update({ id: item.id }, item, {}, (err, _numOfUpdated: number) => {
88-
if (err) {
89-
reject(err);
90-
} else {
91-
this.retrieveOne(item.id)
92-
.then((updated: IAppStorageItem) => resolve(updated))
93-
.catch((err2: Error) => reject(err2));
94-
}
95-
});
96-
});
97-
}
98-
9989
public remove(id: string): Promise<{ success: boolean }> {
10090
return new Promise((resolve, reject) => {
10191
this.db.remove({ id }, (err) => {
@@ -107,4 +97,27 @@ export class TestsAppStorage extends AppMetadataStorage {
10797
});
10898
});
10999
}
100+
101+
public updatePartialAndReturnDocument(
102+
item: Partial<IAppStorageItem>,
103+
options?: { unsetPermissionsGranted?: boolean },
104+
): Promise<IAppStorageItem> {
105+
throw new Error('Method not implemented.');
106+
}
107+
108+
public updateStatus(_id: string, status: AppStatus): Promise<boolean> {
109+
throw new Error('Method not implemented.');
110+
}
111+
112+
public updateSetting(_id: string, setting: ISetting): Promise<boolean> {
113+
throw new Error('Method not implemented.');
114+
}
115+
116+
public updateAppInfo(_id: string, info: IAppInfo): Promise<boolean> {
117+
throw new Error('Method not implemented.');
118+
}
119+
120+
public updateMarketplaceInfo(_id: string, marketplaceInfo: IMarketplaceInfo[]): Promise<boolean> {
121+
throw new Error('Method not implemented.');
122+
}
110123
}

0 commit comments

Comments
 (0)