@wireapp/core 46.46.6-beta.14.f6fd03fe6 → 46.46.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Account.d.ts +156 -51
- package/lib/Account.d.ts.map +1 -1
- package/lib/Account.js +503 -127
- package/lib/Account.test.js +158 -147
- package/lib/broadcast/AvailabilityType.d.ts +1 -1
- package/lib/broadcast/AvailabilityType.d.ts.map +1 -1
- package/lib/broadcast/BroadcastService.d.ts +1 -1
- package/lib/broadcast/BroadcastService.d.ts.map +1 -1
- package/lib/broadcast/BroadcastService.js +1 -1
- package/lib/client/ClientService.d.ts +4 -3
- package/lib/client/ClientService.d.ts.map +1 -1
- package/lib/client/ClientService.js +19 -5
- package/lib/conversation/AbortReason.d.ts +1 -1
- package/lib/conversation/AbortReason.d.ts.map +1 -1
- package/lib/conversation/AssetService/AssetService.d.ts +12 -30
- package/lib/conversation/AssetService/AssetService.d.ts.map +1 -1
- package/lib/conversation/AssetService/AssetService.js +1 -10
- package/lib/conversation/AssetService/AssetService.test.js +8 -3
- package/lib/conversation/ClientActionType.d.ts +1 -1
- package/lib/conversation/ClientActionType.d.ts.map +1 -1
- package/lib/conversation/ClientActionType.js +1 -1
- package/lib/conversation/ConversationService/ConversationService.d.ts +98 -14
- package/lib/conversation/ConversationService/ConversationService.d.ts.map +1 -1
- package/lib/conversation/ConversationService/ConversationService.js +314 -101
- package/lib/conversation/ConversationService/ConversationService.test.js +441 -47
- package/lib/conversation/ConversationService/ConversationService.types.d.ts +5 -4
- package/lib/conversation/ConversationService/ConversationService.types.d.ts.map +1 -1
- package/lib/conversation/ConversationService/Utility/getConversationQualifiedMembers.d.ts.map +1 -1
- package/lib/conversation/ConversationService/Utility/getConversationQualifiedMembers.js +6 -3
- package/lib/conversation/SubconversationService/SubconversationService.d.ts.map +1 -1
- package/lib/conversation/SubconversationService/SubconversationService.js +158 -11
- package/lib/conversation/SubconversationService/SubconversationService.test.js +8 -2
- package/lib/conversation/content/AssetContent.d.ts +1 -1
- package/lib/conversation/content/AssetContent.d.ts.map +1 -1
- package/lib/conversation/content/ButtonActionConfirmationContent.d.ts +1 -1
- package/lib/conversation/content/ButtonActionConfirmationContent.d.ts.map +1 -1
- package/lib/conversation/content/ButtonActionContent.d.ts +1 -1
- package/lib/conversation/content/ButtonActionContent.d.ts.map +1 -1
- package/lib/conversation/content/ClearedContent.d.ts +1 -1
- package/lib/conversation/content/ClearedContent.d.ts.map +1 -1
- package/lib/conversation/content/ClientActionContent.d.ts +1 -1
- package/lib/conversation/content/ClientActionContent.d.ts.map +1 -1
- package/lib/conversation/content/CompositeContent.d.ts +1 -1
- package/lib/conversation/content/CompositeContent.d.ts.map +1 -1
- package/lib/conversation/content/ConfirmationContent.d.ts +1 -1
- package/lib/conversation/content/ConfirmationContent.d.ts.map +1 -1
- package/lib/conversation/content/DeletedContent.d.ts +1 -1
- package/lib/conversation/content/DeletedContent.d.ts.map +1 -1
- package/lib/conversation/content/HiddenContent.d.ts +1 -1
- package/lib/conversation/content/HiddenContent.d.ts.map +1 -1
- package/lib/conversation/content/KnockContent.d.ts +1 -1
- package/lib/conversation/content/KnockContent.d.ts.map +1 -1
- package/lib/conversation/content/LinkPreviewContent.d.ts +1 -1
- package/lib/conversation/content/LinkPreviewContent.d.ts.map +1 -1
- package/lib/conversation/content/MentionContent.d.ts +1 -1
- package/lib/conversation/content/MentionContent.d.ts.map +1 -1
- package/lib/conversation/content/MultipartContent.d.ts +1 -1
- package/lib/conversation/content/MultipartContent.d.ts.map +1 -1
- package/lib/conversation/content/QuoteContent.d.ts +1 -1
- package/lib/conversation/content/QuoteContent.d.ts.map +1 -1
- package/lib/conversation/content/TweetContent.d.ts +1 -1
- package/lib/conversation/content/TweetContent.d.ts.map +1 -1
- package/lib/conversation/content/index.d.ts +1 -1
- package/lib/conversation/content/index.d.ts.map +1 -1
- package/lib/conversation/content/index.js +1 -1
- package/lib/conversation/message/MessageBuilder.d.ts +1 -1
- package/lib/conversation/message/MessageBuilder.d.ts.map +1 -1
- package/lib/conversation/message/MessageBuilder.js +1 -1
- package/lib/conversation/message/MessageService.d.ts.map +1 -1
- package/lib/conversation/message/MessageService.js +1 -1
- package/lib/conversation/message/MessageService.test.js +7 -1
- package/lib/conversation/message/MessageToProtoMapper.d.ts +1 -1
- package/lib/conversation/message/MessageToProtoMapper.d.ts.map +1 -1
- package/lib/conversation/message/MessageToProtoMapper.js +1 -1
- package/lib/conversation/message/messageSender.js +2 -2
- package/lib/cryptography/AssetCryptography/EncryptedAsset.d.ts +2 -2
- package/lib/cryptography/AssetCryptography/EncryptedAsset.d.ts.map +1 -1
- package/lib/messagingProtocols/common.types.d.ts +9 -0
- package/lib/messagingProtocols/common.types.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.d.ts +2 -2
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIService.types.js +2 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.d.ts +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.js +13 -11
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceExternal.test.js +21 -16
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.d.ts +9 -3
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/E2EIServiceInternal.js +31 -12
- package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.d.ts +6 -0
- package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/E2EIdentityService/Helper/index.js +19 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.d.ts +7 -0
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/IncomingProposalsQueue.js +48 -0
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingProposalsQueue/index.d.ts +2 -0
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/{IncomingMessagesQueue → IncomingProposalsQueue}/index.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/{IncomingMessagesQueue → IncomingProposalsQueue}/index.js +1 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.d.ts +0 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/index.js +0 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.js +23 -14
- package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.js +5 -2
- package/lib/messagingProtocols/mls/EventHandler/events/welcomeMessage/welcomeMessage.test.js +13 -3
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts +38 -2
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.js +41 -6
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.d.ts +2 -0
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/MLSService/CoreCryptoMLSError.test.js +124 -0
- package/lib/messagingProtocols/mls/MLSService/MLSService.d.ts +38 -34
- package/lib/messagingProtocols/mls/MLSService/MLSService.d.ts.map +1 -1
- package/lib/messagingProtocols/mls/MLSService/MLSService.js +267 -208
- package/lib/messagingProtocols/mls/MLSService/MLSService.test.js +157 -160
- package/lib/messagingProtocols/mls/MLSService/commitBundleUtil.js +3 -3
- package/lib/messagingProtocols/mls/MLSService/commitBundleUtil.test.js +5 -5
- package/lib/messagingProtocols/mls/conversationRejoinQueue.js +2 -2
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts +78 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.js +173 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts +2 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsErrorMapper.test.js +117 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts +167 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.js +317 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts +2 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/MlsRecoveryOrchestrator.test.js +248 -0
- package/lib/messagingProtocols/mls/recovery/index.d.ts +5 -0
- package/lib/messagingProtocols/mls/recovery/index.d.ts.map +1 -0
- package/lib/messagingProtocols/mls/recovery/index.js +28 -0
- package/lib/messagingProtocols/mls/types.d.ts +0 -8
- package/lib/messagingProtocols/mls/types.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/EventHandler/events/otrMessageAdd/otrMessageAdd.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/EventHandler/events/otrMessageAdd/otrMessageAdd.js +7 -1
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts +8 -15
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CoreCryptoWrapper/CoreCryptoWrapper.js +97 -62
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CryptoClient.types.d.ts +0 -6
- package/lib/messagingProtocols/proteus/ProteusService/CryptoClient/CryptoClient.types.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.d.ts +1 -6
- package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/DecryptionErrorGenerator/DecryptionErrorGenerator.js +19 -22
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.d.ts +5 -3
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.js +11 -24
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.d.ts +1 -0
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.mocks.js +11 -2
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.test.js +13 -9
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.types.d.ts +3 -2
- package/lib/messagingProtocols/proteus/ProteusService/ProteusService.types.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/WithMockedGenerics.test.js +11 -4
- package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.d.ts +0 -4
- package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/cryptoMigrationStateStore.js +0 -5
- package/lib/messagingProtocols/proteus/ProteusService/identityClearer.d.ts +2 -1
- package/lib/messagingProtocols/proteus/ProteusService/identityClearer.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/ProteusService/identityClearer.js +8 -2
- package/lib/messagingProtocols/proteus/Utility/SessionHandler/SessionHandler.test.js +4 -0
- package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.d.ts +1 -1
- package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.d.ts.map +1 -1
- package/lib/messagingProtocols/proteus/Utility/getGenericMessageParams.js +1 -1
- package/lib/notification/NotificationService.d.ts +20 -6
- package/lib/notification/NotificationService.d.ts.map +1 -1
- package/lib/notification/NotificationService.js +23 -14
- package/lib/notification/NotificationService.test.js +8 -0
- package/lib/secretStore/secretKeyGenerator.d.ts +1 -0
- package/lib/secretStore/secretKeyGenerator.d.ts.map +1 -1
- package/lib/secretStore/secretKeyGenerator.js +3 -1
- package/lib/self/SelfService.d.ts +2 -2
- package/lib/self/SelfService.d.ts.map +1 -1
- package/lib/self/SelfService.test.js +5 -2
- package/lib/team/TeamService.d.ts +5 -2
- package/lib/team/TeamService.d.ts.map +1 -1
- package/lib/team/TeamService.js +12 -2
- package/lib/test/StoreHelper.d.ts +2 -0
- package/lib/test/StoreHelper.d.ts.map +1 -0
- package/lib/test/StoreHelper.js +27 -0
- package/lib/user/UserService.d.ts +2 -2
- package/lib/user/UserService.d.ts.map +1 -1
- package/lib/user/UserService.js +3 -3
- package/lib/util/TypePredicateUtil.d.ts.map +1 -1
- package/lib/util/TypePredicateUtil.js +2 -2
- package/package.json +3 -3
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.d.ts +0 -4
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.d.ts.map +0 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/IncomingMesssagesQueue.js +0 -69
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/IncomingMessagesQueue/index.d.ts +0 -2
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.d.ts +0 -2
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.d.ts.map +0 -1
- package/lib/messagingProtocols/mls/EventHandler/events/messageAdd/messageAdd.test.js +0 -98
|
@@ -45,10 +45,13 @@ const client_1 = require("@wireapp/api-client/lib/client");
|
|
|
45
45
|
const conversation_1 = require("@wireapp/api-client/lib/conversation");
|
|
46
46
|
const event_1 = require("@wireapp/api-client/lib/event");
|
|
47
47
|
const http_1 = require("@wireapp/api-client/lib/http");
|
|
48
|
+
const team_1 = require("@wireapp/api-client/lib/team");
|
|
48
49
|
const http_status_codes_1 = require("http-status-codes");
|
|
49
50
|
const api_client_1 = require("@wireapp/api-client");
|
|
51
|
+
const core_crypto_1 = require("@wireapp/core-crypto");
|
|
50
52
|
const __1 = require("..");
|
|
51
53
|
const CoreCryptoMLSError_1 = require("../../messagingProtocols/mls/MLSService/CoreCryptoMLSError");
|
|
54
|
+
const MLSService_1 = require("../../messagingProtocols/mls/MLSService/MLSService");
|
|
52
55
|
const MessagingProtocols = __importStar(require("../../messagingProtocols/proteus"));
|
|
53
56
|
const CoreDB_1 = require("../../storage/CoreDB");
|
|
54
57
|
const PayloadHelper = __importStar(require("../../test/PayloadHelper"));
|
|
@@ -86,9 +89,14 @@ const mockedProteusService = {
|
|
|
86
89
|
encryptGenericMessage: () => Promise.resolve(),
|
|
87
90
|
sendProteusMessage: () => Promise.resolve({ sentAt: new Date() }),
|
|
88
91
|
};
|
|
92
|
+
const apiClients = [];
|
|
89
93
|
describe('ConversationService', () => {
|
|
94
|
+
afterAll(() => {
|
|
95
|
+
apiClients.forEach(client => client.disconnect());
|
|
96
|
+
});
|
|
90
97
|
async function buildConversationService() {
|
|
91
98
|
const client = new api_client_1.APIClient({ urls: api_client_1.APIClient.BACKEND.STAGING });
|
|
99
|
+
apiClients.push(client);
|
|
92
100
|
jest.spyOn(client.api.conversation, 'postMlsMessage').mockReturnValue(Promise.resolve({
|
|
93
101
|
events: [],
|
|
94
102
|
time: new Date().toISOString(),
|
|
@@ -106,13 +114,14 @@ describe('ConversationService', () => {
|
|
|
106
114
|
}));
|
|
107
115
|
jest
|
|
108
116
|
.spyOn(client.api.user, 'getUserSupportedProtocols')
|
|
109
|
-
.mockReturnValue(Promise.resolve([
|
|
117
|
+
.mockReturnValue(Promise.resolve([team_1.CONVERSATION_PROTOCOL.MLS, team_1.CONVERSATION_PROTOCOL.PROTEUS]));
|
|
110
118
|
client.context = {
|
|
111
119
|
clientType: client_1.ClientType.NONE,
|
|
112
120
|
userId: PayloadHelper.getUUID(),
|
|
113
121
|
clientId: PayloadHelper.getUUID(),
|
|
114
122
|
};
|
|
115
123
|
const mockedMLSService = {
|
|
124
|
+
on: jest.fn(),
|
|
116
125
|
encryptMessage: () => { },
|
|
117
126
|
commitPendingProposals: () => Promise.resolve(),
|
|
118
127
|
getEpoch: () => Promise.resolve(),
|
|
@@ -124,8 +133,10 @@ describe('ConversationService', () => {
|
|
|
124
133
|
conversationExists: jest.fn(),
|
|
125
134
|
isConversationEstablished: jest.fn(),
|
|
126
135
|
tryEstablishingMLSGroup: jest.fn(),
|
|
136
|
+
getClientIdsInGroup: jest.fn(),
|
|
127
137
|
getKeyPackagesPayload: jest.fn(),
|
|
128
138
|
addUsersToExistingConversation: jest.fn(),
|
|
139
|
+
removeClientsFromConversation: jest.fn(),
|
|
129
140
|
resetKeyMaterialRenewal: jest.fn(),
|
|
130
141
|
handleMLSWelcomeMessageEvent: jest.fn(),
|
|
131
142
|
};
|
|
@@ -134,7 +145,7 @@ describe('ConversationService', () => {
|
|
|
134
145
|
const mockedSubconversationService = {
|
|
135
146
|
joinConferenceSubconversation: jest.fn(),
|
|
136
147
|
};
|
|
137
|
-
const conversationService = new __1.ConversationService(client, mockedProteusService, mockedDb, groupIdFromConversationId, mockedSubconversationService, mockedMLSService);
|
|
148
|
+
const conversationService = new __1.ConversationService(client, mockedProteusService, mockedDb, groupIdFromConversationId, mockedSubconversationService, () => Promise.resolve(true), mockedMLSService);
|
|
138
149
|
jest.spyOn(conversationService, 'joinByExternalCommit');
|
|
139
150
|
jest.spyOn(conversationService, 'emit');
|
|
140
151
|
return [
|
|
@@ -157,7 +168,7 @@ describe('ConversationService', () => {
|
|
|
157
168
|
const sentTime = new Date().toISOString();
|
|
158
169
|
mockedProteusService.sendMessage = jest.fn().mockResolvedValue({ sentAt: sentTime });
|
|
159
170
|
const promise = conversationService.send({
|
|
160
|
-
protocol:
|
|
171
|
+
protocol: team_1.CONVERSATION_PROTOCOL.PROTEUS,
|
|
161
172
|
conversationId: { id: 'conv1', domain: '' },
|
|
162
173
|
payload: message,
|
|
163
174
|
});
|
|
@@ -166,6 +177,80 @@ describe('ConversationService', () => {
|
|
|
166
177
|
});
|
|
167
178
|
});
|
|
168
179
|
});
|
|
180
|
+
describe('removeUsersFromMLSConversation', () => {
|
|
181
|
+
it('recovers and retries when stale-message occurs during remove users commit upload', async () => {
|
|
182
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
183
|
+
const mockGroupId = 'groupId-stale-remove';
|
|
184
|
+
const mockConversationId = { id: PayloadHelper.getUUID(), domain: 'staging.zinfra.io' };
|
|
185
|
+
const qualifiedUserIds = [
|
|
186
|
+
{ id: 'test-id-1', domain: 'test-domain' },
|
|
187
|
+
{ id: 'test-id-2', domain: 'test-domain' },
|
|
188
|
+
];
|
|
189
|
+
const staleMessageError = {
|
|
190
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
191
|
+
context: {
|
|
192
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
193
|
+
context: {
|
|
194
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
// First removal attempt fails with stale, second succeeds
|
|
199
|
+
jest
|
|
200
|
+
.spyOn(mlsService, 'removeClientsFromConversation')
|
|
201
|
+
.mockRejectedValueOnce(staleMessageError)
|
|
202
|
+
.mockResolvedValueOnce(undefined);
|
|
203
|
+
const remoteEpoch = 6;
|
|
204
|
+
const localEpoch = 5;
|
|
205
|
+
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
206
|
+
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
207
|
+
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValue({
|
|
208
|
+
qualified_id: mockConversationId,
|
|
209
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
210
|
+
epoch: remoteEpoch,
|
|
211
|
+
group_id: mockGroupId,
|
|
212
|
+
});
|
|
213
|
+
await conversationService.removeUsersFromMLSConversation({
|
|
214
|
+
groupId: mockGroupId,
|
|
215
|
+
conversationId: mockConversationId,
|
|
216
|
+
qualifiedUserIds,
|
|
217
|
+
});
|
|
218
|
+
expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
|
|
219
|
+
expect(mlsService.removeClientsFromConversation).toHaveBeenCalledTimes(2);
|
|
220
|
+
expect(mlsService.resetKeyMaterialRenewal).toHaveBeenCalledWith(mockGroupId);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('joinByExternalCommit', () => {
|
|
224
|
+
it('retries join when stale-message occurs during external commit join', async () => {
|
|
225
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
226
|
+
const conversationId = { id: 'conv-join-stale', domain: 'staging.zinfra.io' };
|
|
227
|
+
const staleMessageError = {
|
|
228
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
229
|
+
context: {
|
|
230
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
231
|
+
context: {
|
|
232
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
jest
|
|
237
|
+
.spyOn(mlsService, 'joinByExternalCommit')
|
|
238
|
+
.mockRejectedValueOnce(staleMessageError)
|
|
239
|
+
.mockResolvedValueOnce(undefined);
|
|
240
|
+
const remoteEpoch = 10;
|
|
241
|
+
const localEpoch = 9;
|
|
242
|
+
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
243
|
+
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
244
|
+
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
245
|
+
qualified_id: conversationId,
|
|
246
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
247
|
+
epoch: remoteEpoch,
|
|
248
|
+
group_id: 'gid-join-stale',
|
|
249
|
+
});
|
|
250
|
+
await conversationService.joinByExternalCommit(conversationId);
|
|
251
|
+
expect(mlsService.joinByExternalCommit).toHaveBeenCalledTimes(2);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
169
254
|
describe('"send MLS"', () => {
|
|
170
255
|
const groupId = PayloadHelper.getUUID();
|
|
171
256
|
const messages = [
|
|
@@ -181,7 +266,7 @@ describe('ConversationService', () => {
|
|
|
181
266
|
it(`calls callbacks when sending '${type}' message is starting and successful`, async () => {
|
|
182
267
|
const [conversationService] = await buildConversationService();
|
|
183
268
|
const promise = conversationService.send({
|
|
184
|
-
protocol:
|
|
269
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
185
270
|
groupId,
|
|
186
271
|
payload: message,
|
|
187
272
|
conversationId: { id: '', domain: '' },
|
|
@@ -190,26 +275,62 @@ describe('ConversationService', () => {
|
|
|
190
275
|
expect(result.state).toBe(__1.MessageSendingState.OUTGOING_SENT);
|
|
191
276
|
});
|
|
192
277
|
});
|
|
193
|
-
it('rejoins a MLS group when
|
|
278
|
+
it('rejoins a MLS group when stale-message error occurs during commit bundle upload', async () => {
|
|
194
279
|
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
195
280
|
const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
|
|
196
281
|
const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
|
|
197
282
|
const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
|
|
283
|
+
const staleMessageError = new conversation_1.MLSStaleMessageError('', http_1.BackendErrorLabel.MLS_STALE_MESSAGE, http_status_codes_1.StatusCodes.CONFLICT);
|
|
284
|
+
// First attempt to upload commit bundle fails with stale-message, second attempt succeeds
|
|
198
285
|
jest
|
|
199
|
-
.spyOn(
|
|
200
|
-
.mockRejectedValueOnce(
|
|
286
|
+
.spyOn(mlsService, 'commitPendingProposals')
|
|
287
|
+
.mockRejectedValueOnce(staleMessageError)
|
|
288
|
+
.mockResolvedValueOnce(undefined);
|
|
289
|
+
const remoteEpoch = 5;
|
|
290
|
+
const localEpoch = 4;
|
|
291
|
+
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
292
|
+
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
293
|
+
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
294
|
+
qualified_id: mockConversationId,
|
|
295
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
296
|
+
epoch: remoteEpoch,
|
|
297
|
+
group_id: mockGroupId,
|
|
298
|
+
});
|
|
299
|
+
await conversationService.send({
|
|
300
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
301
|
+
groupId: mockGroupId,
|
|
302
|
+
payload: mockedMessage,
|
|
303
|
+
conversationId: mockConversationId,
|
|
304
|
+
});
|
|
305
|
+
// Recovery via external commit should have been triggered
|
|
306
|
+
expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
|
|
307
|
+
expect(conversationService.emit).toHaveBeenCalledWith('MLSConversationRecovered', {
|
|
308
|
+
conversationId: mockConversationId,
|
|
309
|
+
});
|
|
310
|
+
// Because the failure happened before posting the message, postMlsMessage should be called only once (after recovery)
|
|
311
|
+
expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(1);
|
|
312
|
+
// commitPendingProposals is called twice: first fails, second succeeds
|
|
313
|
+
expect(mlsService.commitPendingProposals).toHaveBeenCalledTimes(2);
|
|
314
|
+
});
|
|
315
|
+
it('rejoins a MLS group when failed encrypting MLS message', async () => {
|
|
316
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
317
|
+
const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
|
|
318
|
+
const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
|
|
319
|
+
const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
|
|
320
|
+
const staleMessageError = new conversation_1.MLSStaleMessageError('', http_1.BackendErrorLabel.MLS_STALE_MESSAGE, http_status_codes_1.StatusCodes.CONFLICT);
|
|
321
|
+
jest.spyOn(apiClient.api.conversation, 'postMlsMessage').mockRejectedValueOnce(staleMessageError);
|
|
201
322
|
const remoteEpoch = 5;
|
|
202
323
|
const localEpoch = 4;
|
|
203
324
|
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
204
325
|
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
205
326
|
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
206
327
|
qualified_id: mockConversationId,
|
|
207
|
-
protocol:
|
|
328
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
208
329
|
epoch: remoteEpoch,
|
|
209
330
|
group_id: mockGroupId,
|
|
210
331
|
});
|
|
211
332
|
await conversationService.send({
|
|
212
|
-
protocol:
|
|
333
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
213
334
|
groupId: mockGroupId,
|
|
214
335
|
payload: mockedMessage,
|
|
215
336
|
conversationId: mockConversationId,
|
|
@@ -220,6 +341,34 @@ describe('ConversationService', () => {
|
|
|
220
341
|
});
|
|
221
342
|
expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(2);
|
|
222
343
|
});
|
|
344
|
+
it('adds missing users to MLS group and retries when group is out of sync during send', async () => {
|
|
345
|
+
const [conversationService, { apiClient }] = await buildConversationService();
|
|
346
|
+
const mockGroupId = 'AAEAAH87aajaQ011i+rNLmwpy0sAZGl5YS53aXJlamxpbms=';
|
|
347
|
+
const mockConversationId = { id: 'mockConversationId', domain: 'staging.zinfra.io' };
|
|
348
|
+
const mockedMessage = MessageBuilder.buildTextMessage({ text: 'test' });
|
|
349
|
+
const missingUsers = [
|
|
350
|
+
{ id: 'user-1', domain: 'staging.zinfra.io' },
|
|
351
|
+
{ id: 'user-2', domain: 'staging.zinfra.io' },
|
|
352
|
+
];
|
|
353
|
+
const outOfSyncError = new conversation_1.MLSGroupOutOfSyncError(http_status_codes_1.StatusCodes.CONFLICT, missingUsers, http_1.BackendErrorLabel.MLS_GROUP_OUT_OF_SYNC);
|
|
354
|
+
// First send fails with out-of-sync, second succeeds via default mock
|
|
355
|
+
jest.spyOn(apiClient.api.conversation, 'postMlsMessage').mockRejectedValueOnce(outOfSyncError);
|
|
356
|
+
const addUsersSpy = jest
|
|
357
|
+
.spyOn(conversationService, 'performAddUsersToMLSConversationAPI')
|
|
358
|
+
.mockResolvedValueOnce({ conversation: { members: { others: [] } } });
|
|
359
|
+
await conversationService.send({
|
|
360
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
361
|
+
groupId: mockGroupId,
|
|
362
|
+
payload: mockedMessage,
|
|
363
|
+
conversationId: mockConversationId,
|
|
364
|
+
});
|
|
365
|
+
expect(addUsersSpy).toHaveBeenCalledWith({
|
|
366
|
+
groupId: mockGroupId,
|
|
367
|
+
conversationId: mockConversationId,
|
|
368
|
+
qualifiedUsers: missingUsers,
|
|
369
|
+
});
|
|
370
|
+
expect(apiClient.api.conversation.postMlsMessage).toHaveBeenCalledTimes(2);
|
|
371
|
+
});
|
|
223
372
|
});
|
|
224
373
|
describe('handleConversationsEpochMismatch', () => {
|
|
225
374
|
beforeEach(() => {
|
|
@@ -229,7 +378,7 @@ describe('ConversationService', () => {
|
|
|
229
378
|
return {
|
|
230
379
|
group_id: 'group-id',
|
|
231
380
|
qualified_id: { id: conversationId || 'conversation-id', domain: 'staging.zinfra.io' },
|
|
232
|
-
protocol:
|
|
381
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
233
382
|
epoch,
|
|
234
383
|
};
|
|
235
384
|
};
|
|
@@ -279,7 +428,7 @@ describe('ConversationService', () => {
|
|
|
279
428
|
const remoteEpoch = 1;
|
|
280
429
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
281
430
|
qualified_id: mockConversationId,
|
|
282
|
-
protocol:
|
|
431
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
283
432
|
epoch: remoteEpoch,
|
|
284
433
|
group_id: mockGroupId,
|
|
285
434
|
});
|
|
@@ -298,19 +447,19 @@ describe('ConversationService', () => {
|
|
|
298
447
|
const updatedEpoch = 2;
|
|
299
448
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
300
449
|
qualified_id: mockConversationId,
|
|
301
|
-
protocol:
|
|
450
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
302
451
|
epoch: remoteEpoch,
|
|
303
452
|
group_id: mockGroupId,
|
|
304
453
|
});
|
|
305
454
|
// The 2nd request we make after joining the conversation with external commit
|
|
306
455
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
307
456
|
qualified_id: mockConversationId,
|
|
308
|
-
protocol:
|
|
457
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
309
458
|
epoch: updatedEpoch,
|
|
310
459
|
group_id: mockGroupId,
|
|
311
460
|
});
|
|
312
461
|
jest.spyOn(mlsService, 'isConversationEstablished').mockResolvedValueOnce(false);
|
|
313
|
-
jest.spyOn(mlsService, 'joinByExternalCommit')
|
|
462
|
+
jest.spyOn(mlsService, 'joinByExternalCommit');
|
|
314
463
|
const establishedConversation = await conversationService.establishMLS1to1Conversation(mockGroupId, selfUser, otherUserId);
|
|
315
464
|
expect(mlsService.registerConversation).not.toHaveBeenCalled();
|
|
316
465
|
expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
|
|
@@ -326,14 +475,14 @@ describe('ConversationService', () => {
|
|
|
326
475
|
const updatedEpoch = 1;
|
|
327
476
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
328
477
|
qualified_id: mockConversationId,
|
|
329
|
-
protocol:
|
|
478
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
330
479
|
epoch: remoteEpoch,
|
|
331
480
|
group_id: mockGroupId,
|
|
332
481
|
});
|
|
333
482
|
// The 2nd request we make after successfully registering a group
|
|
334
483
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
335
484
|
qualified_id: mockConversationId,
|
|
336
|
-
protocol:
|
|
485
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
337
486
|
epoch: updatedEpoch,
|
|
338
487
|
group_id: mockGroupId,
|
|
339
488
|
});
|
|
@@ -355,21 +504,21 @@ describe('ConversationService', () => {
|
|
|
355
504
|
const updatedEpoch = 1;
|
|
356
505
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
357
506
|
qualified_id: mockConversationId,
|
|
358
|
-
protocol:
|
|
507
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
359
508
|
epoch: remoteEpoch,
|
|
360
509
|
group_id: mockGroupId,
|
|
361
510
|
});
|
|
362
511
|
// The 2nd request we make when retrying to register the conversation
|
|
363
512
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
364
513
|
qualified_id: mockConversationId,
|
|
365
|
-
protocol:
|
|
514
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
366
515
|
epoch: remoteEpoch,
|
|
367
516
|
group_id: mockGroupId,
|
|
368
517
|
});
|
|
369
518
|
// The 3rd request we make after successfully registering a group
|
|
370
519
|
jest.spyOn(apiClient.api.conversation, 'getMLS1to1Conversation').mockResolvedValueOnce({
|
|
371
520
|
qualified_id: mockConversationId,
|
|
372
|
-
protocol:
|
|
521
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
373
522
|
epoch: updatedEpoch,
|
|
374
523
|
group_id: mockGroupId,
|
|
375
524
|
});
|
|
@@ -398,7 +547,7 @@ describe('ConversationService', () => {
|
|
|
398
547
|
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
399
548
|
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
400
549
|
qualified_id: conversationId,
|
|
401
|
-
protocol:
|
|
550
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
402
551
|
epoch: remoteEpoch,
|
|
403
552
|
group_id: mockGroupId,
|
|
404
553
|
});
|
|
@@ -432,19 +581,46 @@ describe('ConversationService', () => {
|
|
|
432
581
|
expect(subconversationService.joinConferenceSubconversation).toHaveBeenCalledWith(conversationId, 'groupId');
|
|
433
582
|
});
|
|
434
583
|
it('joins a MLS conversation if it was sent an orphan welcome message', async () => {
|
|
435
|
-
const [conversationService, {
|
|
584
|
+
const [conversationService, { mlsService }] = await buildConversationService();
|
|
436
585
|
const conversationId = { id: 'conversationId', domain: 'staging.zinfra.io' };
|
|
437
586
|
const mockMLSWelcomeMessageEvent = createMLSWelcomeMessageEventMock(conversationId);
|
|
438
587
|
const orphanWelcomeMessageError = new Error();
|
|
439
|
-
|
|
588
|
+
// Simulate core-crypto orphan welcome classification for mapper
|
|
589
|
+
orphanWelcomeMessageError.name = core_crypto_1.MlsErrorType.OrphanWelcome;
|
|
590
|
+
orphanWelcomeMessageError.context = { type: core_crypto_1.MlsErrorType.OrphanWelcome };
|
|
591
|
+
orphanWelcomeMessageError.type = core_crypto_1.ErrorType.Mls;
|
|
440
592
|
jest.spyOn(mlsService, 'handleMLSWelcomeMessageEvent').mockRejectedValueOnce(orphanWelcomeMessageError);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
protocol: conversation_1.ConversationProtocol.MLS,
|
|
444
|
-
});
|
|
593
|
+
// Ensure welcome processing is attempted (succeeds after recovery join)
|
|
594
|
+
jest.spyOn(mlsService, 'handleMLSWelcomeMessageEvent').mockResolvedValueOnce(undefined);
|
|
445
595
|
await conversationService.handleEvent(mockMLSWelcomeMessageEvent);
|
|
446
596
|
await new Promise(resolve => setImmediate(resolve));
|
|
447
|
-
|
|
597
|
+
// Orchestrator triggers a low-level join (performJoinByExternalCommitAPI -> mlsService.joinByExternalCommit)
|
|
598
|
+
expect(mlsService.joinByExternalCommit).toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
it('wipes local MLS state and retries when welcome fails with ConversationAlreadyExists', async () => {
|
|
601
|
+
const [conversationService, { mlsService }] = await buildConversationService();
|
|
602
|
+
const conversationId = { id: 'conversationId', domain: 'staging.zinfra.io' };
|
|
603
|
+
const mockMLSWelcomeMessageEvent = createMLSWelcomeMessageEventMock(conversationId);
|
|
604
|
+
// Create a base64 group id and its byte-array form to simulate core-crypto error context
|
|
605
|
+
const expectedGroupId = 'AXNhbXBsZQ=='; // base64 for '\u0001sample'
|
|
606
|
+
const conversationIdArray = Array.from(Buffer.from(expectedGroupId, 'base64'));
|
|
607
|
+
const existsError = {};
|
|
608
|
+
// Simulate core-crypto "conversation already exists" classification for the mapper
|
|
609
|
+
existsError.type = core_crypto_1.ErrorType.Mls;
|
|
610
|
+
existsError.context = {
|
|
611
|
+
type: core_crypto_1.MlsErrorType.ConversationAlreadyExists,
|
|
612
|
+
context: { conversationId: conversationIdArray },
|
|
613
|
+
};
|
|
614
|
+
const welcomeSpy = jest
|
|
615
|
+
.spyOn(mlsService, 'handleMLSWelcomeMessageEvent')
|
|
616
|
+
.mockRejectedValueOnce(existsError)
|
|
617
|
+
.mockResolvedValueOnce(undefined);
|
|
618
|
+
await conversationService.handleEvent(mockMLSWelcomeMessageEvent);
|
|
619
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
620
|
+
// Expect a single wipe with the extracted group id, and a single retry of welcome handling
|
|
621
|
+
expect(mlsService.wipeConversation).toHaveBeenCalledTimes(1);
|
|
622
|
+
expect(mlsService.wipeConversation).toHaveBeenCalledWith(expectedGroupId);
|
|
623
|
+
expect(welcomeSpy).toHaveBeenCalledTimes(2);
|
|
448
624
|
});
|
|
449
625
|
});
|
|
450
626
|
describe('getConversations', () => {
|
|
@@ -511,21 +687,22 @@ describe('ConversationService', () => {
|
|
|
511
687
|
.map(() => ({ id: PayloadHelper.getUUID(), domain: 'local.wire.com' }));
|
|
512
688
|
const selfUserToAdd = { id: 'self-user-id', domain: 'local.wire.com', skipOwnClientId: apiClient.clientId };
|
|
513
689
|
const qualifiedUsers = [...otherUsersToAdd, selfUserToAdd];
|
|
514
|
-
jest
|
|
690
|
+
jest
|
|
691
|
+
.spyOn(mlsService, 'getKeyPackagesPayload')
|
|
692
|
+
.mockResolvedValueOnce({ keyPackages: [new Uint8Array(0)], failures: [] });
|
|
515
693
|
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
516
694
|
qualified_id: mockConversationId,
|
|
517
|
-
protocol:
|
|
695
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
518
696
|
epoch: 1,
|
|
519
697
|
group_id: mockGroupId,
|
|
520
698
|
});
|
|
521
|
-
|
|
522
|
-
jest.spyOn(mlsService, 'addUsersToExistingConversation').mockResolvedValueOnce(mlsMessage);
|
|
699
|
+
jest.spyOn(mlsService, 'addUsersToExistingConversation');
|
|
523
700
|
await conversationService.addUsersToMLSConversation({
|
|
524
701
|
qualifiedUsers,
|
|
525
702
|
groupId: mockGroupId,
|
|
526
703
|
conversationId: mockConversationId,
|
|
527
704
|
});
|
|
528
|
-
expect(mlsService.getKeyPackagesPayload).toHaveBeenCalledWith(qualifiedUsers);
|
|
705
|
+
expect(mlsService.getKeyPackagesPayload).toHaveBeenCalledWith(qualifiedUsers, undefined);
|
|
529
706
|
expect(mlsService.resetKeyMaterialRenewal).toHaveBeenCalledWith(mockGroupId);
|
|
530
707
|
});
|
|
531
708
|
it('should return failure reasons for users it was not possible to claim keys for', async () => {
|
|
@@ -541,20 +718,11 @@ describe('ConversationService', () => {
|
|
|
541
718
|
reason: __1.AddUsersFailureReasons.OFFLINE_FOR_TOO_LONG,
|
|
542
719
|
users: [otherUsersToAdd[0]],
|
|
543
720
|
};
|
|
544
|
-
const addUsersFailure = {
|
|
545
|
-
reason: __1.AddUsersFailureReasons.UNREACHABLE_BACKENDS,
|
|
546
|
-
users: [otherUsersToAdd[1]],
|
|
547
|
-
backends: [otherUsersToAdd[1].domain],
|
|
548
|
-
};
|
|
549
|
-
const mlsFailure = {
|
|
550
|
-
reason: __1.AddUsersFailureReasons.NOT_MLS_CAPABLE,
|
|
551
|
-
users: [otherUsersToAdd[2]],
|
|
552
|
-
};
|
|
553
721
|
jest.spyOn(apiClient.api.user, 'getUserSupportedProtocols').mockImplementation(id => {
|
|
554
722
|
if (id === otherUsersToAdd[2]) {
|
|
555
|
-
return Promise.resolve([
|
|
723
|
+
return Promise.resolve([team_1.CONVERSATION_PROTOCOL.PROTEUS]);
|
|
556
724
|
}
|
|
557
|
-
return Promise.resolve([
|
|
725
|
+
return Promise.resolve([team_1.CONVERSATION_PROTOCOL.MLS, team_1.CONVERSATION_PROTOCOL.PROTEUS]);
|
|
558
726
|
});
|
|
559
727
|
jest.spyOn(mlsService, 'getKeyPackagesPayload').mockResolvedValueOnce({
|
|
560
728
|
keyPackages: [new Uint8Array(0)],
|
|
@@ -562,18 +730,63 @@ describe('ConversationService', () => {
|
|
|
562
730
|
});
|
|
563
731
|
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
564
732
|
qualified_id: mockConversationId,
|
|
565
|
-
protocol:
|
|
733
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
566
734
|
epoch: 1,
|
|
567
735
|
group_id: mockGroupId,
|
|
568
736
|
});
|
|
569
|
-
|
|
570
|
-
jest.spyOn(mlsService, 'addUsersToExistingConversation').mockResolvedValueOnce(mlsMessage);
|
|
737
|
+
jest.spyOn(mlsService, 'addUsersToExistingConversation');
|
|
571
738
|
const { failedToAdd } = await conversationService.addUsersToMLSConversation({
|
|
572
739
|
qualifiedUsers,
|
|
573
740
|
groupId: mockGroupId,
|
|
574
741
|
conversationId: mockConversationId,
|
|
575
742
|
});
|
|
576
|
-
expect(failedToAdd).toEqual([keysClaimingFailure
|
|
743
|
+
expect(failedToAdd).toEqual([keysClaimingFailure]);
|
|
744
|
+
});
|
|
745
|
+
it('recovers and retries when stale-message occurs during add users commit upload', async () => {
|
|
746
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
747
|
+
const mockGroupId = 'groupId-stale-add';
|
|
748
|
+
const mockConversationId = { id: PayloadHelper.getUUID(), domain: 'local.wire.com' };
|
|
749
|
+
const otherUsersToAdd = Array(2)
|
|
750
|
+
.fill(0)
|
|
751
|
+
.map(() => ({ id: PayloadHelper.getUUID(), domain: 'local.wire.com' }));
|
|
752
|
+
const qualifiedUsers = [...otherUsersToAdd];
|
|
753
|
+
const staleMessageError = {
|
|
754
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
755
|
+
context: {
|
|
756
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
757
|
+
context: {
|
|
758
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
const getKPSpy = jest.spyOn(mlsService, 'getKeyPackagesPayload');
|
|
763
|
+
getKPSpy.mockResolvedValue({
|
|
764
|
+
keyPackages: [new Uint8Array(0)],
|
|
765
|
+
failures: [],
|
|
766
|
+
});
|
|
767
|
+
// Simulate commit upload failing once with stale, then succeeding
|
|
768
|
+
jest
|
|
769
|
+
.spyOn(mlsService, 'addUsersToExistingConversation')
|
|
770
|
+
.mockRejectedValueOnce(staleMessageError)
|
|
771
|
+
.mockResolvedValueOnce(undefined);
|
|
772
|
+
const remoteEpoch = 5;
|
|
773
|
+
const localEpoch = 4;
|
|
774
|
+
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
775
|
+
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
776
|
+
const getConvSpy = jest.spyOn(apiClient.api.conversation, 'getConversation');
|
|
777
|
+
getConvSpy.mockResolvedValue({
|
|
778
|
+
qualified_id: mockConversationId,
|
|
779
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
780
|
+
epoch: remoteEpoch,
|
|
781
|
+
group_id: mockGroupId,
|
|
782
|
+
});
|
|
783
|
+
await conversationService.addUsersToMLSConversation({
|
|
784
|
+
qualifiedUsers,
|
|
785
|
+
groupId: mockGroupId,
|
|
786
|
+
conversationId: mockConversationId,
|
|
787
|
+
});
|
|
788
|
+
expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(mockConversationId);
|
|
789
|
+
expect(mlsService.addUsersToExistingConversation).toHaveBeenCalledTimes(2);
|
|
577
790
|
});
|
|
578
791
|
});
|
|
579
792
|
describe('tryEstablishingMLSGroup', () => {
|
|
@@ -620,6 +833,186 @@ describe('ConversationService', () => {
|
|
|
620
833
|
expect(conversationService.addUsersToMLSConversation).not.toHaveBeenCalled();
|
|
621
834
|
});
|
|
622
835
|
});
|
|
836
|
+
describe('reactToKeyMaterialUpdateFailure', () => {
|
|
837
|
+
function getKeyMaterialFailureHandler(mlsService) {
|
|
838
|
+
const onMock = mlsService.on;
|
|
839
|
+
const call = onMock.mock.calls.find(([event]) => event === MLSService_1.MLSServiceEvents.KEY_MATERIAL_UPDATE_FAILURE);
|
|
840
|
+
expect(call).toBeTruthy();
|
|
841
|
+
return call[1];
|
|
842
|
+
}
|
|
843
|
+
it('resets a broken MLS conversation', async () => {
|
|
844
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
845
|
+
const groupId = 'group-1';
|
|
846
|
+
const qualified_id = { id: 'conv-1', domain: 'staging.zinfra.io' };
|
|
847
|
+
jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
|
|
848
|
+
found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
|
|
849
|
+
});
|
|
850
|
+
const resetSpy = jest
|
|
851
|
+
.spyOn(conversationService, 'handleBrokenMLSConversation')
|
|
852
|
+
.mockResolvedValue(undefined);
|
|
853
|
+
const addUsersSpy = jest.spyOn(conversationService, 'addUsersToMLSConversation');
|
|
854
|
+
const handler = getKeyMaterialFailureHandler(mlsService);
|
|
855
|
+
const brokenErr = {
|
|
856
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
857
|
+
context: {
|
|
858
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
859
|
+
context: {
|
|
860
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.BROKEN_MLS_CONVERSATION }),
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
await handler({ error: brokenErr, groupId });
|
|
865
|
+
expect(resetSpy).toHaveBeenCalledWith(qualified_id);
|
|
866
|
+
expect(addUsersSpy).not.toHaveBeenCalled();
|
|
867
|
+
});
|
|
868
|
+
it('adds missing users when group is out of sync', async () => {
|
|
869
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
870
|
+
const groupId = 'group-2';
|
|
871
|
+
const qualified_id = { id: 'conv-2', domain: 'staging.zinfra.io' };
|
|
872
|
+
const missingUsers = [
|
|
873
|
+
{ id: 'u1', domain: 'staging.zinfra.io' },
|
|
874
|
+
{ id: 'u2', domain: 'staging.zinfra.io' },
|
|
875
|
+
];
|
|
876
|
+
jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
|
|
877
|
+
found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
|
|
878
|
+
});
|
|
879
|
+
const addUsersSpy = jest
|
|
880
|
+
.spyOn(conversationService, 'performAddUsersToMLSConversationAPI')
|
|
881
|
+
.mockResolvedValue({ conversation: {} });
|
|
882
|
+
const resetSpy = jest
|
|
883
|
+
.spyOn(conversationService, 'handleBrokenMLSConversation')
|
|
884
|
+
.mockResolvedValue(undefined);
|
|
885
|
+
const handler = getKeyMaterialFailureHandler(mlsService);
|
|
886
|
+
const outOfSyncErr = {
|
|
887
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
888
|
+
context: {
|
|
889
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
890
|
+
context: {
|
|
891
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({
|
|
892
|
+
message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_GROUP_OUT_OF_SYNC,
|
|
893
|
+
missing_users: missingUsers,
|
|
894
|
+
}),
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
await handler({ error: outOfSyncErr, groupId });
|
|
899
|
+
expect(addUsersSpy).toHaveBeenCalledWith({
|
|
900
|
+
groupId,
|
|
901
|
+
conversationId: qualified_id,
|
|
902
|
+
qualifiedUsers: missingUsers,
|
|
903
|
+
});
|
|
904
|
+
expect(resetSpy).not.toHaveBeenCalled();
|
|
905
|
+
});
|
|
906
|
+
it('deduplicates concurrent recoveries for the same group id', async () => {
|
|
907
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
908
|
+
const groupId = 'group-dup';
|
|
909
|
+
const qualified_id = { id: 'conv-dup', domain: 'staging.zinfra.io' };
|
|
910
|
+
jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValue({
|
|
911
|
+
found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 1 }],
|
|
912
|
+
});
|
|
913
|
+
// Make the recovery hang until we resolve it, to simulate overlapping calls
|
|
914
|
+
let resolveDeferred;
|
|
915
|
+
const deferred = new Promise(res => (resolveDeferred = res));
|
|
916
|
+
const resetSpy = jest
|
|
917
|
+
.spyOn(conversationService, 'handleBrokenMLSConversation')
|
|
918
|
+
.mockReturnValue(deferred);
|
|
919
|
+
const handler = getKeyMaterialFailureHandler(mlsService);
|
|
920
|
+
const brokenErr = {
|
|
921
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
922
|
+
context: {
|
|
923
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
924
|
+
context: {
|
|
925
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.BROKEN_MLS_CONVERSATION }),
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
const p1 = handler({ error: brokenErr, groupId });
|
|
930
|
+
const p2 = handler({ error: brokenErr, groupId });
|
|
931
|
+
// Complete the first recovery
|
|
932
|
+
if (resolveDeferred) {
|
|
933
|
+
resolveDeferred();
|
|
934
|
+
}
|
|
935
|
+
await Promise.allSettled([p1, p2]);
|
|
936
|
+
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
937
|
+
});
|
|
938
|
+
it('handles stale-message by rejoining via external commit and emits recovery event', async () => {
|
|
939
|
+
const [conversationService, { apiClient, mlsService }] = await buildConversationService();
|
|
940
|
+
const groupId = 'group-stale';
|
|
941
|
+
const qualified_id = { id: 'conv-stale', domain: 'staging.zinfra.io' };
|
|
942
|
+
jest.spyOn(apiClient.api.conversation, 'getConversationList').mockResolvedValueOnce({
|
|
943
|
+
found: [{ group_id: groupId, qualified_id, protocol: team_1.CONVERSATION_PROTOCOL.MLS, epoch: 2 }],
|
|
944
|
+
});
|
|
945
|
+
const handler = getKeyMaterialFailureHandler(mlsService);
|
|
946
|
+
const staleMessageError = {
|
|
947
|
+
type: core_crypto_1.ErrorType.Mls,
|
|
948
|
+
context: {
|
|
949
|
+
type: core_crypto_1.MlsErrorType.MessageRejected,
|
|
950
|
+
context: {
|
|
951
|
+
reason: (0, CoreCryptoMLSError_1.serializeAbortReason)({ message: CoreCryptoMLSError_1.UPLOAD_COMMIT_BUNDLE_ABORT_REASONS.MLS_STALE_MESSAGE }),
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
const remoteEpoch = 3;
|
|
956
|
+
const localEpoch = 2;
|
|
957
|
+
jest.spyOn(mlsService, 'conversationExists').mockResolvedValueOnce(true);
|
|
958
|
+
jest.spyOn(mlsService, 'getEpoch').mockResolvedValueOnce(localEpoch);
|
|
959
|
+
jest.spyOn(apiClient.api.conversation, 'getConversation').mockResolvedValueOnce({
|
|
960
|
+
qualified_id,
|
|
961
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
962
|
+
epoch: remoteEpoch,
|
|
963
|
+
group_id: groupId,
|
|
964
|
+
});
|
|
965
|
+
await handler({ error: staleMessageError, groupId });
|
|
966
|
+
// Expect a rejoin on stale and a recovery event
|
|
967
|
+
expect(conversationService.joinByExternalCommit).toHaveBeenCalledWith(qualified_id);
|
|
968
|
+
expect(conversationService.emit).toHaveBeenCalledWith('MLSConversationRecovered', { conversationId: qualified_id });
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
describe('groupIdConversationMap cache', () => {
|
|
972
|
+
function makeConversation(group_id, id) {
|
|
973
|
+
return {
|
|
974
|
+
group_id,
|
|
975
|
+
qualified_id: { id, domain: 'staging.zinfra.io' },
|
|
976
|
+
protocol: team_1.CONVERSATION_PROTOCOL.MLS,
|
|
977
|
+
epoch: 1,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
it('refreshGroupIdConversationMap builds the cache from backend list', async () => {
|
|
981
|
+
const [conversationService, { apiClient }] = await buildConversationService();
|
|
982
|
+
const conv1 = makeConversation('g-1', 'c-1');
|
|
983
|
+
const conv2 = makeConversation('g-2', 'c-2');
|
|
984
|
+
const getListSpy = jest
|
|
985
|
+
.spyOn(apiClient.api.conversation, 'getConversationList')
|
|
986
|
+
.mockResolvedValueOnce({ found: [conv1, conv2] });
|
|
987
|
+
await conversationService.refreshGroupIdConversationMap();
|
|
988
|
+
expect(getListSpy).toHaveBeenCalledTimes(1);
|
|
989
|
+
// Validate through the private cache map
|
|
990
|
+
const cache = conversationService.groupIdConversationMap;
|
|
991
|
+
expect(cache.get('g-1')?.qualified_id.id).toBe('c-1');
|
|
992
|
+
expect(cache.get('g-2')?.qualified_id.id).toBe('c-2');
|
|
993
|
+
});
|
|
994
|
+
it('getConversationByGroupId uses cache when available without backend call', async () => {
|
|
995
|
+
const [conversationService, { apiClient }] = await buildConversationService();
|
|
996
|
+
const conv = makeConversation('g-hit', 'c-hit');
|
|
997
|
+
// Pre-populate cache
|
|
998
|
+
conversationService.groupIdConversationMap.set('g-hit', conv);
|
|
999
|
+
const getListSpy = jest
|
|
1000
|
+
.spyOn(apiClient.api.conversation, 'getConversationList')
|
|
1001
|
+
.mockImplementation(() => Promise.reject(new Error('should not be called')));
|
|
1002
|
+
const result = await conversationService.getConversationByGroupId('g-hit');
|
|
1003
|
+
expect(result?.qualified_id.id).toBe('c-hit');
|
|
1004
|
+
expect(getListSpy).not.toHaveBeenCalled();
|
|
1005
|
+
});
|
|
1006
|
+
it('getConversationByGroupId refreshes on cache miss and returns undefined if not found', async () => {
|
|
1007
|
+
const [conversationService, { apiClient }] = await buildConversationService();
|
|
1008
|
+
const getListSpy = jest
|
|
1009
|
+
.spyOn(apiClient.api.conversation, 'getConversationList')
|
|
1010
|
+
.mockResolvedValueOnce({ found: [makeConversation('g-else', 'c-else')] });
|
|
1011
|
+
const result = await conversationService.getConversationByGroupId('g-missing');
|
|
1012
|
+
expect(result).toBeUndefined();
|
|
1013
|
+
expect(getListSpy).toHaveBeenCalledTimes(1);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
623
1016
|
});
|
|
624
1017
|
function generateImage() {
|
|
625
1018
|
const image = {
|
|
@@ -636,6 +1029,7 @@ function generateImage() {
|
|
|
636
1029
|
keyBytes: Buffer.from([]),
|
|
637
1030
|
sha256: Buffer.from([]),
|
|
638
1031
|
token: '',
|
|
1032
|
+
domain: 'example.com',
|
|
639
1033
|
},
|
|
640
1034
|
};
|
|
641
1035
|
}
|