Skip to content

Commit

Permalink
feat: support of named profiles (#1149)
Browse files Browse the repository at this point in the history
  • Loading branch information
saintsebastian authored and rpl committed Oct 29, 2018
1 parent ecb3ecc commit c85aa6e
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 10 deletions.
56 changes: 55 additions & 1 deletion src/firefox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,47 @@ export function configureProfile(
return Promise.resolve(profile);
}

export type getProfileFn = (profileName: string) => Promise<string | void>;

export type CreateProfileFinderParams = {|
userDirectoryPath?: string,
FxProfile?: typeof FirefoxProfile
|}

export function defaultCreateProfileFinder(
{
userDirectoryPath,
FxProfile = FirefoxProfile,
}: CreateProfileFinderParams = {}
): getProfileFn {
const finder = new FxProfile.Finder(userDirectoryPath);
const readProfiles = promisify(finder.readProfiles, finder);
const getPath = promisify(finder.getPath, finder);
return async (profileName: string): Promise<string | void> => {
try {
await readProfiles();
const hasProfileName = finder.profiles.filter(
(profileDef) => profileDef.Name === profileName).length !== 0;
if (hasProfileName) {
return await getPath(profileName);
}
} catch (error) {
if (!isErrorWithCode('ENOENT', error)) {
throw error;
}
log.warn('Unable to find Firefox profiles.ini');
}
};
}

// useProfile types and implementation.

export type UseProfileParams = {
app?: PreferencesAppName,
configureThisProfile?: ConfigureProfileFn,
isFirefoxDefaultProfile?: IsDefaultProfileFn,
customPrefs?: FirefoxPreferences,
createProfileFinder?: typeof defaultCreateProfileFinder,
};

// Use the target path as a Firefox profile without cloning it
Expand All @@ -335,6 +369,7 @@ export async function useProfile(
configureThisProfile = configureProfile,
isFirefoxDefaultProfile = isDefaultProfile,
customPrefs = {},
createProfileFinder = defaultCreateProfileFinder,
}: UseProfileParams = {},
): Promise<FirefoxProfile> {
const isForbiddenProfile = await isFirefoxDefaultProfile(profilePath);
Expand All @@ -346,7 +381,26 @@ export async function useProfile(
'\nSee https://github.com/mozilla/web-ext/issues/1005'
);
}
const profile = new FirefoxProfile({destinationDirectory: profilePath});

let destinationDirectory;
const getProfilePath = createProfileFinder();

const profileIsDirPath = await isDirectory(profilePath);
if (profileIsDirPath) {
log.debug(`Using profile directory "${profilePath}"`);
destinationDirectory = profilePath;
} else {
log.debug(`Assuming ${profilePath} is a named profile`);
destinationDirectory = await getProfilePath(profilePath);
if (!destinationDirectory) {
throw new UsageError(
`The request "${profilePath}" profile name ` +
'cannot be resolved to a profile path'
);
}
}

const profile = new FirefoxProfile({destinationDirectory});
return await configureThisProfile(profile, {app, customPrefs});
}

Expand Down
219 changes: 210 additions & 9 deletions tests/unit/test-firefox/test.firefox.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
fake,
makeSureItFails,
TCPConnectError,
ErrorWithCode,
} from '../helpers';
import {manifestWithoutApps} from '../test-util/test.manifest';
import {RemoteFirefox} from '../../../src/firefox/remote';
Expand Down Expand Up @@ -511,40 +512,240 @@ describe('firefox', () => {
} catch (error) {
exception = error;
}

assert.instanceOf(exception, UsageError);
assert.match(
exception && exception.message,
/Cannot use --keep-profile-changes on a default profile/
);
});

it('rejects to a UsageError when profile is not found',
async () => {
const fakeGetProfilePath = sinon.spy(() => Promise.resolve(false));
const createProfileFinder = () => {
return fakeGetProfilePath;
};
const isFirefoxDefaultProfile = sinon.spy(
() => Promise.resolve(false)
);

const promise = firefox.useProfile('profileName', {
createProfileFinder,
isFirefoxDefaultProfile,
});

await assert.isRejected(promise, UsageError);
await assert.isRejected(
promise,
/The request "profileName" profile name cannot be resolved/
);
}
);

it('resolves to a FirefoxProfile instance', () => withBaseProfile(
(baseProfile) => {
async (baseProfile) => {
const configureThisProfile = (profile) => Promise.resolve(profile);
return firefox.useProfile(baseProfile.path(), {configureThisProfile})
.then((profile) => {
assert.instanceOf(profile, FirefoxProfile);
});
const createProfileFinder = () => {
return (profilePath) => Promise.resolve(profilePath);
};
const profile = await firefox.useProfile(baseProfile.path(), {
configureThisProfile,
createProfileFinder,
});
assert.instanceOf(profile, FirefoxProfile);
}
));

it('looks for profile path if passed a name', () => withBaseProfile(
async (baseProfile) => {
const fakeGetProfilePath = sinon.spy(() => baseProfile.path());
const createProfileFinder = () => {
return fakeGetProfilePath;
};
const isFirefoxDefaultProfile = sinon.spy(
() => Promise.resolve(false)
);
await firefox.useProfile('profileName', {
createProfileFinder,
isFirefoxDefaultProfile,
});
sinon.assert.calledOnce(fakeGetProfilePath);
sinon.assert.calledWith(
fakeGetProfilePath,
sinon.match('profileName')
);
}
));

it('checks if named profile is default', () => withBaseProfile(
async (baseProfile) => {
const createProfileFinder = () => {
return () => Promise.resolve(baseProfile.path());
};
const isFirefoxDefaultProfile = sinon.spy(
() => Promise.resolve(false)
);
await firefox.useProfile('profileName', {
createProfileFinder,
isFirefoxDefaultProfile,
});
sinon.assert.calledOnce(isFirefoxDefaultProfile);
sinon.assert.calledWith(
isFirefoxDefaultProfile,
sinon.match('profileName')
);
}
));

it('configures a profile', () => withBaseProfile(
(baseProfile) => {
const configureThisProfile =
sinon.spy((profile) => Promise.resolve(profile));
const app = 'fennec';
const profilePath = baseProfile.path();
return firefox.useProfile(profilePath, {app, configureThisProfile})
return firefox.useProfile(profilePath, {configureThisProfile})
.then((profile) => {
sinon.assert.called(configureThisProfile);
sinon.assert.calledWith(configureThisProfile, profile);
assert.equal(configureThisProfile.firstCall.args[1].app, app);
});
}
));

});

describe('defaultCreateProfileFinder', () => {

function prepareReaderTest(readProfileReturns) {
const fakeReadProfiles = sinon.spy(() => {
return readProfileReturns;
});

const fakeGetPath = sinon.spy(() => Promise.resolve());
const fakeProfiles = [{Name: 'someName'}];
const userDirectoryPath = '/non/existent/path';

const FxProfile = {
Finder() {
return {
readProfiles: fakeReadProfiles,
getPath: fakeGetPath,
profiles: fakeProfiles,
};
},
};

return {
fakeReadProfiles,
fakeGetPath,
fakeProfiles,
FxProfile,
userDirectoryPath,
};
}

it('creates a finder', async () => {
const FxProfile = {
Finder: sinon.spy(function() {
return {};
}),
};
firefox.defaultCreateProfileFinder({FxProfile});
sinon.assert.calledWith(FxProfile.Finder, sinon.match(undefined));
});

it('creates finder based on userDirectoryPath if present', async () => {
const FxProfile = {
Finder: sinon.spy(function() {
return {};
}),
};
const userDirectoryPath = '/non/existent/path';
firefox.defaultCreateProfileFinder({userDirectoryPath, FxProfile});

sinon.assert.called(FxProfile.Finder);
sinon.assert.calledWith(
FxProfile.Finder,
sinon.match(userDirectoryPath),
);
});

it('returns a finder that resolves a profile name', async () => {
const {
fakeReadProfiles,
fakeGetPath,
FxProfile,
userDirectoryPath,
} = prepareReaderTest(Promise.resolve());

const getter = firefox.defaultCreateProfileFinder({
userDirectoryPath,
FxProfile,
});

await getter('someName');

sinon.assert.called(fakeReadProfiles);
sinon.assert.called(fakeGetPath);
sinon.assert.calledWith(
fakeGetPath,
sinon.match('someName'),
);
});

it('returns a finder that resolves undefined for no profiles.ini',
async () => {
const {
fakeReadProfiles,
fakeGetPath,
FxProfile,
userDirectoryPath,
} = prepareReaderTest(
Promise.reject(new ErrorWithCode('ENOENT', 'fake ENOENT error'))
);

const getter = firefox.defaultCreateProfileFinder({
userDirectoryPath,
FxProfile,
});

const res = await getter('someName');
assert.equal(
res,
undefined,
'Got an undefined result when the profiles.ini file does not exist');

sinon.assert.called(fakeReadProfiles);
sinon.assert.notCalled(fakeGetPath);
});

it('returns a finder that throws unexpected errors',
async () => {
const {
fakeReadProfiles,
fakeGetPath,
FxProfile,
userDirectoryPath,
} = prepareReaderTest(
Promise.reject(new Error('unspecified error'))
);

const getter = firefox.defaultCreateProfileFinder({
userDirectoryPath,
FxProfile,
});

const promise = getter('someName');

assert.isRejected(
promise,
'unspecified error',
'Throws expected error'
);
sinon.assert.called(fakeReadProfiles);
sinon.assert.notCalled(fakeGetPath);
});

});

describe('configureProfile', () => {

function withTempProfile(callback) {
Expand Down

0 comments on commit c85aa6e

Please sign in to comment.