From 1063f59ddec8e992c018b8926fac0f59e6674728 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 12 May 2020 00:19:46 +0200 Subject: [PATCH] Fix #97555 --- .../common/abstractSynchronizer.ts | 38 ++++-- .../userDataSync/common/extensionsMerge.ts | 3 +- .../userDataSync/common/globalStateMerge.ts | 2 +- .../userDataSync/common/keybindingsSync.ts | 8 +- .../userDataSync/common/settingsSync.ts | 2 +- .../userDataSync/common/snippetsMerge.ts | 2 +- .../userDataSync/common/snippetsSync.ts | 2 +- .../userDataSync/common/userDataSync.ts | 2 +- .../common/userDataSyncService.ts | 2 +- .../test/common/globalStateSync.test.ts | 67 ++++++++-- .../test/common/keybindingsSync.test.ts | 86 +++++++++++++ .../test/common/settingsSync.test.ts | 74 +++++++++-- .../test/common/snippetsSync.test.ts | 119 +++++++++++++----- .../test/common/synchronizer.test.ts | 25 ++-- .../test/common/userDataSyncClient.ts | 4 + .../test/common/userDataSyncService.test.ts | 5 - 16 files changed, 351 insertions(+), 90 deletions(-) create mode 100644 src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index e52d759f8e4..4860d158671 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; import { CancelablePromise } from 'vs/base/common/async'; @@ -108,7 +108,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); } - async sync(ref?: string): Promise { + async sync(manifest: IUserDataManifest | null): Promise { if (!this.isEnabled()) { if (this.status !== SyncStatus.Idle) { await this.stop(); @@ -129,7 +129,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); - const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); + const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); let status: SyncStatus = SyncStatus.Idle; try { @@ -144,6 +144,24 @@ export abstract class AbstractSynchroniser extends Disposable { } } + private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { + if (lastSyncUserData) { + + const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined; + + // Last time synced resource and latest resource on server are same + if (lastSyncUserData.ref === latestRef) { + return lastSyncUserData; + } + + // There is no resource on server and last time it was synced with no resource + if (latestRef === undefined && lastSyncUserData.syncData === null) { + return lastSyncUserData; + } + } + return this.getRemoteUserData(lastSyncUserData); + } + async getSyncPreview(): Promise { if (!this.isEnabled()) { return { hasLocalChanged: false, hasRemoteChanged: false }; @@ -225,15 +243,19 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { /* ignore */ } } - protected async getLastSyncUserData(): Promise { + async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); const parsed = JSON.parse(content.value.toString()); - let syncData: ISyncData = JSON.parse(parsed.content); + const userData: IUserData = parsed as IUserData; + if (userData.content === null) { + return { ref: parsed.ref, syncData: null } as T; + } + let syncData: ISyncData = JSON.parse(userData.content); // Migration from old content to sync data if (!isSyncData(syncData)) { - syncData = { version: this.version, content: parsed.content }; + syncData = { version: this.version, content: userData.content }; } return { ...parsed, ...{ syncData, content: undefined } }; @@ -247,11 +269,11 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { - const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps }; + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } - protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { + async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { const { ref, content } = await this.getUserData(lastSyncData); let syncData: ISyncData | null = null; if (content !== null) { diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index bb02ff61d84..43f246b5733 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -21,11 +21,12 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync const updated: ISyncExtension[] = []; if (!remoteExtensions) { + const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())); return { added, removed, updated, - remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())) + remote: remote.length > 0 ? remote : null }; } diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts index 4d4755fb245..9b7d47a307c 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -18,7 +18,7 @@ export interface IMergeResult { export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, previouslySkipped: string[], logService: ILogService): IMergeResult { if (!remoteStorage) { - return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] }; + return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] }; } const localToRemote = compare(localStorage, remoteStorage); diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index ed6943af053..9d3c69444c9 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -248,10 +248,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); } - if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { + if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null); - await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); + const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null; + await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, content: lastSyncContent } : null }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } @@ -315,7 +315,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } - private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { const parsed = JSON.parse(syncContent); if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index ce6c56c87b5..33b7eae171a 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -357,7 +357,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; } - private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { try { const parsed = JSON.parse(syncContent); return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts index 42a9dfaae05..07c944c53cd 100644 --- a/src/vs/platform/userDataSync/common/snippetsMerge.ts +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -26,7 +26,7 @@ export function merge(local: IStringDictionary, remote: IStringDictionar removed: values(removed), updated, conflicts: [], - remote: local + remote: Object.keys(local).length > 0 ? local : null }; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index d802e543cf4..b8d4a194b8c 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -285,7 +285,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD private async doGeneratePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary = {}, token: CancellationToken = CancellationToken.None): Promise { const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; - const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index eec0c4502a1..e158e4dce61 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -274,7 +274,7 @@ export interface IUserDataSynchroniser { pull(): Promise; push(): Promise; - sync(ref?: string): Promise; + sync(manifest: IUserDataManifest | null): Promise; stop(): Promise; getSyncPreview(): Promise diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 1c1bd056006..bd150093aab 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -136,7 +136,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ for (const synchroniser of this.synchronisers) { try { - await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resource] : undefined); + await synchroniser.sync(manifest); } catch (e) { this.handleSyncError(e, synchroniser.resource); this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]); diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index 9f86b9354a5..7a1f331d5fa 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -44,11 +44,58 @@ suite('GlobalStateSync', () => { teardown(() => disposableStore.clear()); + test('when global state does not exist', async () => { + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when global state is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + updateStorage('a', 'value1', testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(JSON.parse(lastSyncUserData!.syncData!.content).storage, { 'a': { version: 1, value: 'value1' } }); + }); + test('first time sync - outgoing to server (no state)', async () => { updateStorage('a', 'value1', testClient); await updateLocale(testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -63,7 +110,7 @@ suite('GlobalStateSync', () => { await updateLocale(client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -76,7 +123,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -94,7 +141,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('a', 'value2', client2); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -109,10 +156,10 @@ suite('GlobalStateSync', () => { test('sync adding a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -127,10 +174,10 @@ suite('GlobalStateSync', () => { test('sync updating a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('a', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -145,10 +192,10 @@ suite('GlobalStateSync', () => { test('sync removing a storage value', async () => { updateStorage('a', 'value1', testClient); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); removeStorage('b', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts new file mode 100644 index 00000000000..9fd790befce --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { VSBuffer } from 'vs/base/common/buffer'; + +suite('KeybindingsSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + + let testObject: KeybindingsSynchroniser; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(true); + testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Keybindings) as KeybindingsSynchroniser; + disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); + }); + + teardown(() => disposableStore.clear()); + + test('when keybindings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(keybindingsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when keybindings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(keybindingsResource, VSBuffer.fromString('[]')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); + }); + +}); diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index c0f87712cf1..a9e62326673 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -43,13 +43,67 @@ suite('SettingsSync', () => { setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); - await client.setUp(); + await client.setUp(true); testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser; disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); }); teardown(() => disposableStore.clear()); + test('when settings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const settingResource = client.instantiationService.get(IEnvironmentService).settingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(settingResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when settings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + + const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(settingsResource, VSBuffer.fromString('{}')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + }); + test('sync for first time to the server', async () => { const expected = `{ @@ -75,7 +129,7 @@ suite('SettingsSync', () => { }`; await updateSettings(expected); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -99,7 +153,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -130,7 +184,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -161,7 +215,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -185,7 +239,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -203,7 +257,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -237,7 +291,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -285,7 +339,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -333,7 +387,7 @@ suite('SettingsSync', () => { await updateSettings(expected); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('should fail with invalid content error'); } catch (e) { assert.ok(e instanceof UserDataSyncError); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 7943e35359f..87da32e3a01 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -167,11 +167,62 @@ suite('SnippetsSync', () => { teardown(() => disposableStore.clear()); + test('when snippets does not exist', async () => { + const fileService = testClient.instantiationService.get(IFileService); + const snippetsResource = testClient.instantiationService.get(IEnvironmentService).snippetsHome; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(snippetsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when snippet is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + await updateSnippet('html.json', htmlSnippet1, testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(lastSyncUserData!.syncData!.content, JSON.stringify({ 'html.json': htmlSnippet1 })); + }); + test('first time sync - outgoing to server (no snippets)', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -186,7 +237,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -201,7 +252,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -221,7 +272,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -234,7 +285,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet1); @@ -259,7 +310,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -278,7 +329,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); let conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -299,7 +350,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -324,10 +375,10 @@ suite('SnippetsSync', () => { test('sync adding a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -345,12 +396,12 @@ suite('SnippetsSync', () => { test('sync adding a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -362,10 +413,10 @@ suite('SnippetsSync', () => { test('sync updating a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -381,12 +432,12 @@ suite('SnippetsSync', () => { test('sync updating a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -397,13 +448,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); @@ -413,13 +464,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - resolve conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2); assert.equal(testObject.status, SyncStatus.Idle); @@ -437,10 +488,10 @@ suite('SnippetsSync', () => { test('sync removing a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -459,12 +510,12 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -478,13 +529,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -499,13 +550,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -517,13 +568,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3); assert.equal(testObject.status, SyncStatus.Idle); @@ -544,13 +595,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, ''); assert.equal(testObject.status, SyncStatus.Idle); @@ -601,7 +652,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -622,7 +673,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 3c2583e4626..340b95c0f24 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -79,7 +79,7 @@ suite('TestSynchronizer', () => { const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; assert.deepEqual(actual, [SyncStatus.Syncing]); @@ -94,7 +94,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -107,7 +107,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -122,7 +122,7 @@ suite('TestSynchronizer', () => { disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('Should fail'); } catch (e) { assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); @@ -134,12 +134,12 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Syncing); @@ -154,7 +154,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -164,11 +164,11 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -178,7 +178,7 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); // Sync once testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); testObject.syncBarrier = new Barrier(); // update remote data before syncing so that 412 is thrown by server @@ -190,8 +190,9 @@ suite('TestSynchronizer', () => { }); // Start sycing - const { ref } = await userDataSyncStoreService.read(testObject.resource, null); - await testObject.sync(ref); + const manifest = await client.manifest(); + const ref = manifest!.latest![testObject.resource]; + await testObject.sync(await client.manifest()); assert.deepEqual(server.requests, [ { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index d9168c084cc..ae2edfc85d1 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -124,6 +124,10 @@ export class UserDataSyncClient extends Disposable { return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null); } + manifest(): Promise { + return this.instantiationService.get(IUserDataSyncStoreService).manifest(); + } + } export class UserDataSyncTestServer implements IRequestService { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 5c2b51c2840..b7fbb82ed1c 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -45,7 +45,6 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); @@ -71,13 +70,10 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); @@ -384,7 +380,6 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, ]); -- GitLab