Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const nestedOptionTypes = [
'FileDownloadOptions',
'FileUploadOptions',
'IdempotencyOptions',
'InstallationOptions',
'Object',
'PagesCustomUrlsOptions',
'PagesOptions',
Expand All @@ -39,6 +40,7 @@ const nestedOptionEnvPrefix = {
FileDownloadOptions: 'PARSE_SERVER_FILE_DOWNLOAD_',
FileUploadOptions: 'PARSE_SERVER_FILE_UPLOAD_',
IdempotencyOptions: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_',
InstallationOptions: 'PARSE_SERVER_INSTALLATION_',
LiveQueryOptions: 'PARSE_SERVER_LIVEQUERY_',
LiveQueryServerOptions: 'PARSE_LIVE_QUERY_SERVER_',
LogClientEvent: 'PARSE_SERVER_DATABASE_LOG_CLIENT_EVENTS_',
Expand Down
35 changes: 35 additions & 0 deletions spec/Deprecator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,39 @@ describe('Deprecator', () => {
);
}
});

it('registers a deprecation entry for installation.duplicateDeviceTokenActionEnforceAuth', () => {
const Deprecations = require('../lib/Deprecator/Deprecations');
const entry = Deprecations.find(
d => d.optionKey === 'installation.duplicateDeviceTokenActionEnforceAuth'
);
expect(entry).toBeDefined();
expect(entry.changeNewDefault).toBe('true');
expect(entry.solution).toContain('duplicateDeviceTokenActionEnforceAuth');
});

it('logs deprecation for installation.duplicateDeviceTokenActionEnforceAuth when not set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer();
expect(logSpy).toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
changeNewDefault: 'true',
})
);
});

it('does not log deprecation for installation.duplicateDeviceTokenActionEnforceAuth when explicitly set', async () => {
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});

await reconfigureServer({
installation: { duplicateDeviceTokenActionEnforceAuth: false },
});
expect(logSpy).not.toHaveBeenCalledWith(
jasmine.objectContaining({
optionKey: 'installation.duplicateDeviceTokenActionEnforceAuth',
})
);
});
});
295 changes: 295 additions & 0 deletions spec/InstallationDedup.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
'use strict';

const Parse = require('parse/node').Parse;

describe('InstallationDedup', () => {
let InstallationDedup;
let logger;
let logSpy;

beforeEach(() => {
InstallationDedup = require('../lib/InstallationDedup');
logger = require('../lib/logger').logger;
logSpy = {
verbose: spyOn(logger, 'verbose').and.callFake(() => {}),
warn: spyOn(logger, 'warn').and.callFake(() => {}),
error: spyOn(logger, 'error').and.callFake(() => {}),
};
});

describe('removeConflictingDeviceToken', () => {
it('action="delete" with no match resolves silently and logs verbose', async () => {
const database = {
destroy: jasmine
.createSpy('destroy')
.and.returnValue(Promise.reject({ code: Parse.Error.OBJECT_NOT_FOUND })),
};
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'delete',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(database.destroy).toHaveBeenCalled();
expect(logSpy.verbose).toHaveBeenCalled();
expect(logSpy.warn).not.toHaveBeenCalled();
expect(logSpy.error).not.toHaveBeenCalled();
});

it('action="delete" with matches calls destroy with empty options when enforceAuth=false', async () => {
const database = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
};
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'delete',
enforceAuth: false,
runOptions: { acl: ['*'] },
validSchemaController: undefined,
});
expect(database.destroy).toHaveBeenCalledWith(
'_Installation',
{ deviceToken: 'X' },
{},
undefined
);
expect(logSpy.verbose).toHaveBeenCalled();
});

it('action="delete" with enforceAuth=true passes runOptions to destroy', async () => {
const database = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
};
const runOptions = { acl: ['*', 'userABC'] };
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'delete',
enforceAuth: true,
runOptions,
validSchemaController: undefined,
});
expect(database.destroy).toHaveBeenCalledWith(
'_Installation',
{ deviceToken: 'X' },
runOptions,
undefined
);
});

it('action="update" calls update with deviceToken cleared and many=true', async () => {
const database = {
update: jasmine.createSpy('update').and.returnValue(Promise.resolve()),
};
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'update',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(database.update).toHaveBeenCalledWith(
'_Installation',
{ deviceToken: 'X' },
{ deviceToken: { __op: 'Delete' } },
{},
true,
undefined
);
expect(logSpy.verbose).toHaveBeenCalled();
});

it('OPERATION_FORBIDDEN error is swallowed and logged as warn', async () => {
const database = {
destroy: jasmine
.createSpy('destroy')
.and.returnValue(
Promise.reject({ code: Parse.Error.OPERATION_FORBIDDEN, message: 'denied' })
),
};
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'delete',
enforceAuth: true,
runOptions: { acl: ['*'] },
validSchemaController: undefined,
});
expect(logSpy.warn).toHaveBeenCalled();
expect(logSpy.error).not.toHaveBeenCalled();
});

it('unexpected error is logged as error and rethrown', async () => {
const database = {
destroy: jasmine
.createSpy('destroy')
.and.returnValue(Promise.reject(new Error('database connection lost'))),
};
let caught;
try {
await InstallationDedup.removeConflictingDeviceToken({
database,
query: { deviceToken: 'X' },
action: 'delete',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
} catch (e) {
caught = e;
}
expect(caught).toBeDefined();
expect(caught.message).toBe('database connection lost');
expect(logSpy.error).toHaveBeenCalled();
});
});

describe('applyDuplicateDeviceTokenMerge', () => {
const idMatch = { objectId: 'A', installationId: 'I' };
const deviceTokenMatch = { objectId: 'B', deviceToken: 'X' };

it('mergePriority="deviceToken" + action="delete" destroys idMatch and returns deviceTokenMatch.objectId', async () => {
const database = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
};
const result = await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'delete',
mergePriority: 'deviceToken',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(result).toBe('B');
expect(database.destroy).toHaveBeenCalledWith(
'_Installation',
{ objectId: 'A' },
{},
undefined
);
expect(logSpy.verbose).toHaveBeenCalled();
});

it('mergePriority="deviceToken" + action="update" clears installationId on idMatch and returns deviceTokenMatch.objectId', async () => {
const database = {
update: jasmine.createSpy('update').and.returnValue(Promise.resolve()),
};
const result = await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'update',
mergePriority: 'deviceToken',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(result).toBe('B');
expect(database.update).toHaveBeenCalledWith(
'_Installation',
{ objectId: 'A' },
{ installationId: { __op: 'Delete' } },
{},
false,
undefined
);
});

it('mergePriority="installationId" + action="delete" destroys deviceTokenMatch and returns idMatch.objectId', async () => {
const database = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
};
const result = await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'delete',
mergePriority: 'installationId',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(result).toBe('A');
expect(database.destroy).toHaveBeenCalledWith(
'_Installation',
{ objectId: 'B' },
{},
undefined
);
});

it('mergePriority="installationId" + action="update" clears deviceToken on deviceTokenMatch and returns idMatch.objectId', async () => {
const database = {
update: jasmine.createSpy('update').and.returnValue(Promise.resolve()),
};
const result = await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'update',
mergePriority: 'installationId',
enforceAuth: false,
runOptions: {},
validSchemaController: undefined,
});
expect(result).toBe('A');
expect(database.update).toHaveBeenCalledWith(
'_Installation',
{ objectId: 'B' },
{ deviceToken: { __op: 'Delete' } },
{},
false,
undefined
);
});

it('OPERATION_FORBIDDEN on the merge action still returns survivor objectId (silent skip)', async () => {
const database = {
destroy: jasmine
.createSpy('destroy')
.and.returnValue(Promise.reject({ code: Parse.Error.OPERATION_FORBIDDEN })),
};
const result = await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'delete',
mergePriority: 'deviceToken',
enforceAuth: true,
runOptions: { acl: ['*'] },
validSchemaController: undefined,
});
expect(result).toBe('B');
expect(logSpy.warn).toHaveBeenCalled();
});

it('enforceAuth=true passes runOptions to destroy', async () => {
const database = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
};
const runOptions = { acl: ['*', 'userABC'] };
await InstallationDedup.applyDuplicateDeviceTokenMerge({
database,
idMatch,
deviceTokenMatch,
action: 'delete',
mergePriority: 'deviceToken',
enforceAuth: true,
runOptions,
validSchemaController: undefined,
});
expect(database.destroy).toHaveBeenCalledWith(
'_Installation',
{ objectId: 'A' },
runOptions,
undefined
);
});
});
});
Loading
Loading