mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 02:58:31 -05:00
Merge from vscode 9bc92b48d945144abb405b9e8df05e18accb9148
This commit is contained in:
@@ -174,7 +174,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
) {
|
||||
super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
this._register(this.fileService.watch(dirname(file)));
|
||||
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
|
||||
@@ -39,7 +39,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
) {
|
||||
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
|
||||
@@ -14,6 +14,9 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
|
||||
private _onDidChangeToken: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
|
||||
readonly onDidChangeToken: Event<string | undefined> = this._onDidChangeToken.event;
|
||||
|
||||
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
|
||||
|
||||
private _token: string | undefined;
|
||||
|
||||
constructor() {
|
||||
@@ -30,4 +33,8 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
|
||||
this._onDidChangeToken.fire(token);
|
||||
}
|
||||
}
|
||||
|
||||
sendTokenFailed(): void {
|
||||
this._onTokenFailed.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
|
||||
return this.sync(loop, auto);
|
||||
}
|
||||
}
|
||||
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.SessionExpired) {
|
||||
this.logService.info('Auto Sync: Cloud has new session');
|
||||
this.logService.info('Auto Sync: Resetting the local sync state.');
|
||||
await this.userDataSyncService.resetLocal();
|
||||
this.logService.info('Auto Sync: Completed resetting the local sync state.');
|
||||
return this.sync(loop, auto);
|
||||
}
|
||||
this.logService.error(e);
|
||||
this.successiveFailures++;
|
||||
this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown });
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { isEqual, joinPath } from 'vs/base/common/resources';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
|
||||
@@ -120,23 +120,27 @@ export interface IUserData {
|
||||
}
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: string;
|
||||
url: URI;
|
||||
authenticationProviderId: string;
|
||||
}
|
||||
|
||||
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
return value && value.url && value.authenticationProviderId ? value : undefined;
|
||||
const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY);
|
||||
if (value && value.url && value.authenticationProviderId) {
|
||||
return {
|
||||
url: joinPath(URI.parse(value.url), 'v1'),
|
||||
authenticationProviderId: value.authenticationProviderId
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState'];
|
||||
export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState';
|
||||
|
||||
export interface IUserDataManifest {
|
||||
settings: string;
|
||||
keybindings: string;
|
||||
extensions: string;
|
||||
globalState: string;
|
||||
latest?: Record<ResourceKey, string>
|
||||
session: string;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
@@ -162,6 +166,7 @@ export enum UserDataSyncErrorCode {
|
||||
TooLarge = 'TooLarge',
|
||||
NoRef = 'NoRef',
|
||||
TurnedOff = 'TurnedOff',
|
||||
SessionExpired = 'SessionExpired',
|
||||
|
||||
// Local Errors
|
||||
LocalPreconditionFailed = 'LocalPreconditionFailed',
|
||||
@@ -303,9 +308,11 @@ export interface IUserDataAuthTokenService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeToken: Event<string | undefined>;
|
||||
readonly onTokenFailed: Event<void>;
|
||||
|
||||
getToken(): Promise<string | undefined>;
|
||||
setToken(accessToken: string | undefined): Promise<void>;
|
||||
sendTokenFailed(): void;
|
||||
}
|
||||
|
||||
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');
|
||||
|
||||
@@ -96,6 +96,7 @@ export class UserDataAuthTokenServiceChannel implements IServerChannel {
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeToken': return this.service.onDidChangeToken;
|
||||
case 'onTokenFailed': return this.service.onTokenFailed;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,14 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const SESSION_ID_KEY = 'sync.sessionId';
|
||||
|
||||
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
|
||||
|
||||
_serviceBrand: any;
|
||||
@@ -48,6 +51,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
|
||||
@@ -96,7 +100,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
|
||||
const manifest = await this.userDataSyncStoreService.manifest();
|
||||
let manifest = await this.userDataSyncStoreService.manifest();
|
||||
|
||||
// Server has no data but this machine was synced before
|
||||
if (manifest === null && await this.hasPreviouslySynced()) {
|
||||
@@ -104,14 +108,30 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
|
||||
const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
// Server session is different from client session
|
||||
if (sessionId && manifest && sessionId !== manifest.session) {
|
||||
throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
|
||||
}
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
await synchroniser.sync(manifest ? manifest[synchroniser.resourceKey] : undefined);
|
||||
await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined);
|
||||
} catch (e) {
|
||||
this.handleSyncError(e, synchroniser.source);
|
||||
}
|
||||
}
|
||||
|
||||
// After syncing, get the manifest if it was not available before
|
||||
if (manifest === null) {
|
||||
manifest = await this.userDataSyncStoreService.manifest();
|
||||
}
|
||||
|
||||
// Update local session id
|
||||
if (manifest && manifest.session !== sessionId) {
|
||||
this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
|
||||
|
||||
} finally {
|
||||
@@ -140,26 +160,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return synchroniser.accept(content);
|
||||
}
|
||||
|
||||
private async hasPreviouslySynced(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasPreviouslySynced()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async hasLocalData(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasLocalData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
@@ -189,6 +189,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
synchroniser.resetLocal();
|
||||
@@ -199,6 +200,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
private async hasPreviouslySynced(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasPreviouslySynced()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async hasLocalData(): Promise<boolean> {
|
||||
await this.checkEnablement();
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
if (await synchroniser.hasLocalData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async resetRemote(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
|
||||
@@ -33,7 +32,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString();
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', key, 'latest').toString();
|
||||
const headers: IHeaders = {};
|
||||
// Disable caching as they are cached by synchronisers
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
@@ -65,7 +64,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key).toString();
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
if (ref) {
|
||||
headers['If-Match'] = ref;
|
||||
@@ -89,7 +88,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', 'latest').toString();
|
||||
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'application/json' };
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
||||
@@ -105,7 +104,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString();
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
|
||||
@@ -134,6 +133,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
if (context.res.statusCode === 401) {
|
||||
this.authTokenService.sendTokenFailed();
|
||||
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
|
||||
suite('KeybindingsMerge - No Conflicts', () => {
|
||||
|
||||
@@ -613,7 +609,7 @@ suite('KeybindingsMerge - No Conflicts', () => {
|
||||
});
|
||||
|
||||
async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) {
|
||||
const userDataSyncUtilService = new MockUserDataSyncUtilService();
|
||||
const userDataSyncUtilService = new TestUserDataSyncUtilService();
|
||||
const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions();
|
||||
return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService);
|
||||
}
|
||||
@@ -621,22 +617,3 @@ async function mergeKeybindings(localContent: string, remoteContent: string, bas
|
||||
function stringify(value: any): string {
|
||||
return JSON.stringify(value, null, '\t');
|
||||
}
|
||||
|
||||
class MockUserDataSyncUtilService implements IUserDataSyncUtilService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
|
||||
const keys: IStringDictionary<string> = {};
|
||||
for (const keybinding of userbindings) {
|
||||
keys[keybinding] = keybinding;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
|
||||
return { eol: '\n', insertSpaces: false, tabSize: 4 };
|
||||
}
|
||||
|
||||
async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise<void> { }
|
||||
}
|
||||
|
||||
246
src/vs/platform/userDataSync/test/common/userDataSyncClient.ts
Normal file
246
src/vs/platform/userDataSync/test/common/userDataSyncClient.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataAuthTokenService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
|
||||
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService';
|
||||
import { IGlobalExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';
|
||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export class UserDataSyncClient extends Disposable {
|
||||
|
||||
readonly instantiationService: TestInstantiationService;
|
||||
|
||||
constructor(readonly testServer: UserDataSyncTestServer = new UserDataSyncTestServer()) {
|
||||
super();
|
||||
this.instantiationService = new TestInstantiationService();
|
||||
}
|
||||
|
||||
async setUp(empty: boolean = false): Promise<void> {
|
||||
const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory });
|
||||
const userDataSyncHome = joinPath(userDataDirectory, '.sync');
|
||||
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
|
||||
userDataSyncHome,
|
||||
settingsResource: joinPath(userDataDirectory, 'settings.json'),
|
||||
settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'),
|
||||
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
|
||||
keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'),
|
||||
argvResource: joinPath(userDataDirectory, 'argv.json'),
|
||||
});
|
||||
|
||||
const logService = new NullLogService();
|
||||
this.instantiationService.stub(ILogService, logService);
|
||||
|
||||
const fileService = this._register(new FileService(logService));
|
||||
fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider());
|
||||
this.instantiationService.stub(IFileService, fileService);
|
||||
|
||||
this.instantiationService.stub(IStorageService, new InMemoryStorageService());
|
||||
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({
|
||||
'configurationSync.store': {
|
||||
url: this.testServer.url,
|
||||
authenticationProviderId: 'test'
|
||||
}
|
||||
})));
|
||||
|
||||
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
|
||||
await configurationService.initialize();
|
||||
this.instantiationService.stub(IConfigurationService, configurationService);
|
||||
|
||||
this.instantiationService.stub(IRequestService, this.testServer);
|
||||
this.instantiationService.stub(IUserDataAuthTokenService, <Partial<IUserDataAuthTokenService>>{
|
||||
onDidChangeToken: new Emitter<string | undefined>().event,
|
||||
async getToken() { return 'token'; }
|
||||
});
|
||||
|
||||
this.instantiationService.stub(IUserDataSyncLogService, logService);
|
||||
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
|
||||
this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService());
|
||||
this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService));
|
||||
|
||||
this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService));
|
||||
this.instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
|
||||
async getInstalled() { return []; },
|
||||
onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event,
|
||||
onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event,
|
||||
});
|
||||
this.instantiationService.stub(IExtensionGalleryService, <Partial<IExtensionGalleryService>>{
|
||||
isEnabled() { return true; },
|
||||
async getCompatibleExtension() { return null; }
|
||||
});
|
||||
|
||||
this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser));
|
||||
this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService));
|
||||
|
||||
if (empty) {
|
||||
await fileService.del(environmentService.settingsResource);
|
||||
} else {
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
|
||||
}
|
||||
await configurationService.reloadConfiguration();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncTestServer implements IRequestService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly url: string = 'http://host:3000';
|
||||
private session: string | null = null;
|
||||
private readonly data: Map<ResourceKey, IUserData> = new Map<ResourceKey, IUserData>();
|
||||
|
||||
private _requests: { url: string, type: string, headers?: IHeaders }[] = [];
|
||||
get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; }
|
||||
|
||||
private _responses: { status: number }[] = [];
|
||||
get responses(): { status: number }[] { return this._responses; }
|
||||
reset(): void { this._requests = []; this._responses = []; }
|
||||
|
||||
async resolveProxy(url: string): Promise<string | undefined> { return url; }
|
||||
|
||||
async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
const headers: IHeaders = {};
|
||||
if (options.headers) {
|
||||
if (options.headers['If-None-Match']) {
|
||||
headers['If-None-Match'] = options.headers['If-None-Match'];
|
||||
}
|
||||
if (options.headers['If-Match']) {
|
||||
headers['If-Match'] = options.headers['If-Match'];
|
||||
}
|
||||
}
|
||||
this._requests.push({ url: options.url!, type: options.type!, headers });
|
||||
const requestContext = await this.doRequest(options);
|
||||
this._responses.push({ status: requestContext.res.statusCode! });
|
||||
return requestContext;
|
||||
}
|
||||
|
||||
private async doRequest(options: IRequestOptions): Promise<IRequestContext> {
|
||||
const versionUrl = `${this.url}/v1/`;
|
||||
const relativePath = options.url!.indexOf(versionUrl) === 0 ? options.url!.substring(versionUrl.length) : undefined;
|
||||
const segments = relativePath ? relativePath.split('/') : [];
|
||||
if (options.type === 'GET' && segments.length === 1 && segments[0] === 'manifest') {
|
||||
return this.getManifest(options.headers);
|
||||
}
|
||||
if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource' && segments[2] === 'latest') {
|
||||
return this.getLatestData(segments[1], options.headers);
|
||||
}
|
||||
if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') {
|
||||
return this.writeData(segments[1], options.data, options.headers);
|
||||
}
|
||||
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') {
|
||||
return this.clear(options.headers);
|
||||
}
|
||||
return this.toResponse(501);
|
||||
}
|
||||
|
||||
private async getManifest(headers?: IHeaders): Promise<IRequestContext> {
|
||||
if (this.session) {
|
||||
const latest: Record<ResourceKey, string> = Object.create({});
|
||||
const manifest: IUserDataManifest = { session: this.session, latest };
|
||||
this.data.forEach((value, key) => latest[key] = value.ref);
|
||||
return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest));
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private async getLatestData(resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
|
||||
const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource);
|
||||
if (resourceKey) {
|
||||
const data = this.data.get(resourceKey);
|
||||
if (!data) {
|
||||
return this.toResponse(204, { etag: '0' });
|
||||
}
|
||||
if (headers['If-None-Match'] === data.ref) {
|
||||
return this.toResponse(304);
|
||||
}
|
||||
return this.toResponse(200, { etag: data.ref }, data.content || '');
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> {
|
||||
if (!headers['If-Match']) {
|
||||
return this.toResponse(428);
|
||||
}
|
||||
if (!this.session) {
|
||||
this.session = generateUuid();
|
||||
}
|
||||
const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource);
|
||||
if (resourceKey) {
|
||||
const data = this.data.get(resourceKey);
|
||||
if (headers['If-Match'] !== (data ? data.ref : '0')) {
|
||||
return this.toResponse(412);
|
||||
}
|
||||
const ref = `${parseInt(data?.ref || '0') + 1}`;
|
||||
this.data.set(resourceKey, { ref, content });
|
||||
return this.toResponse(200, { etag: ref });
|
||||
}
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private async clear(headers?: IHeaders): Promise<IRequestContext> {
|
||||
this.data.clear();
|
||||
this.session = null;
|
||||
return this.toResponse(204);
|
||||
}
|
||||
|
||||
private toResponse(statusCode: number, headers?: IHeaders, data?: string): IRequestContext {
|
||||
return {
|
||||
res: {
|
||||
headers: headers || {},
|
||||
statusCode
|
||||
},
|
||||
stream: bufferToStream(VSBuffer.fromString(data || ''))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TestUserDataSyncUtilService implements IUserDataSyncUtilService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
|
||||
const keys: IStringDictionary<string> = {};
|
||||
for (const keybinding of userbindings) {
|
||||
keys[keybinding] = keybinding;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
|
||||
return { eol: '\n', insertSpaces: false, tabSize: 4 };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,555 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
suite('UserDataSyncService', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test first time sync ever', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, 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: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync ever with no data', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp(true);
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync for first time
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// 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: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with no changes - pull', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncWithMerge();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with changes - pull', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncWithMerge();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with no changes - merge', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncWithMerge();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with changes - merge', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (merge) from the test client
|
||||
target.reset();
|
||||
await testObject.isFirstTimeSyncWithMerge();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test sync when there are no changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// sync from the client again
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test sync when there are local changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
target.reset();
|
||||
|
||||
// Do changes in the client
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync from the client
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test sync when there are remote changes', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Sync from test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Do changes in first client and sync
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Sync from test client
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test delete', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from the client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the client
|
||||
target.reset();
|
||||
await testObject.reset();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test delete and sync', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Sync from the client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the client
|
||||
await testObject.reset();
|
||||
|
||||
// Sync again
|
||||
target.reset();
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, 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: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test delete on one client throws turned off error on other client while syncing', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the first client
|
||||
await client.instantiationService.get(IUserDataSyncService).reset();
|
||||
|
||||
// Sync from the test client
|
||||
target.reset();
|
||||
try {
|
||||
await testObject.sync();
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof UserDataSyncError);
|
||||
assert.deepEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.TurnedOff);
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
throw assert.fail('Should fail with turned off error');
|
||||
});
|
||||
|
||||
test('test creating new session from one client throws session expired error on another client while syncing', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Set up and sync from the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Set up and sync from the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// Reset from the first client
|
||||
await client.instantiationService.get(IUserDataSyncService).reset();
|
||||
|
||||
// Sync again from the first client to create new session
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Sync from the test client
|
||||
target.reset();
|
||||
try {
|
||||
await testObject.sync();
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof UserDataSyncError);
|
||||
assert.deepEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.SessionExpired);
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
throw assert.fail('Should fail with turned off error');
|
||||
});
|
||||
|
||||
test('test sync status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup the client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
const testObject = client.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// sync from the client
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await testObject.sync();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
|
||||
});
|
||||
|
||||
test('test sync conflicts status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
fileService = testClient.instantiationService.get(IFileService);
|
||||
environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// sync from the client
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.deepEqual(testObject.conflictsSources, [SyncSource.Settings]);
|
||||
});
|
||||
|
||||
test('test sync will sync other non conflicted areas', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client and get conflicts in settings
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
let testFileService = testClient.instantiationService.get(IFileService);
|
||||
let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// sync from the first client with changes in keybindings
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// sync from the test client
|
||||
target.reset();
|
||||
const actualStatuses: SyncStatus[] = [];
|
||||
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
|
||||
await testObject.sync();
|
||||
|
||||
disposable.dispose();
|
||||
assert.deepEqual(actualStatuses, []);
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('test stop sync reset status', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
let fileService = client.instantiationService.get(IFileService);
|
||||
let environmentService = client.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await client.instantiationService.get(IUserDataSyncService).sync();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
fileService = testClient.instantiationService.get(IFileService);
|
||||
environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
await testObject.sync();
|
||||
|
||||
// sync from the client
|
||||
await testObject.stop();
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflictsSources, []);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user