Skip to content

Commit c85aa6e

Browse files
saintsebastianrpl
authored andcommitted
feat: support of named profiles (#1149)
1 parent ecb3ecc commit c85aa6e

File tree

2 files changed

+265
-10
lines changed

2 files changed

+265
-10
lines changed

src/firefox/index.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,47 @@ export function configureProfile(
317317
return Promise.resolve(profile);
318318
}
319319

320+
export type getProfileFn = (profileName: string) => Promise<string | void>;
321+
322+
export type CreateProfileFinderParams = {|
323+
userDirectoryPath?: string,
324+
FxProfile?: typeof FirefoxProfile
325+
|}
326+
327+
export function defaultCreateProfileFinder(
328+
{
329+
userDirectoryPath,
330+
FxProfile = FirefoxProfile,
331+
}: CreateProfileFinderParams = {}
332+
): getProfileFn {
333+
const finder = new FxProfile.Finder(userDirectoryPath);
334+
const readProfiles = promisify(finder.readProfiles, finder);
335+
const getPath = promisify(finder.getPath, finder);
336+
return async (profileName: string): Promise<string | void> => {
337+
try {
338+
await readProfiles();
339+
const hasProfileName = finder.profiles.filter(
340+
(profileDef) => profileDef.Name === profileName).length !== 0;
341+
if (hasProfileName) {
342+
return await getPath(profileName);
343+
}
344+
} catch (error) {
345+
if (!isErrorWithCode('ENOENT', error)) {
346+
throw error;
347+
}
348+
log.warn('Unable to find Firefox profiles.ini');
349+
}
350+
};
351+
}
352+
320353
// useProfile types and implementation.
321354

322355
export type UseProfileParams = {
323356
app?: PreferencesAppName,
324357
configureThisProfile?: ConfigureProfileFn,
325358
isFirefoxDefaultProfile?: IsDefaultProfileFn,
326359
customPrefs?: FirefoxPreferences,
360+
createProfileFinder?: typeof defaultCreateProfileFinder,
327361
};
328362

329363
// Use the target path as a Firefox profile without cloning it
@@ -335,6 +369,7 @@ export async function useProfile(
335369
configureThisProfile = configureProfile,
336370
isFirefoxDefaultProfile = isDefaultProfile,
337371
customPrefs = {},
372+
createProfileFinder = defaultCreateProfileFinder,
338373
}: UseProfileParams = {},
339374
): Promise<FirefoxProfile> {
340375
const isForbiddenProfile = await isFirefoxDefaultProfile(profilePath);
@@ -346,7 +381,26 @@ export async function useProfile(
346381
'\nSee https://github.com/mozilla/web-ext/issues/1005'
347382
);
348383
}
349-
const profile = new FirefoxProfile({destinationDirectory: profilePath});
384+
385+
let destinationDirectory;
386+
const getProfilePath = createProfileFinder();
387+
388+
const profileIsDirPath = await isDirectory(profilePath);
389+
if (profileIsDirPath) {
390+
log.debug(`Using profile directory "${profilePath}"`);
391+
destinationDirectory = profilePath;
392+
} else {
393+
log.debug(`Assuming ${profilePath} is a named profile`);
394+
destinationDirectory = await getProfilePath(profilePath);
395+
if (!destinationDirectory) {
396+
throw new UsageError(
397+
`The request "${profilePath}" profile name ` +
398+
'cannot be resolved to a profile path'
399+
);
400+
}
401+
}
402+
403+
const profile = new FirefoxProfile({destinationDirectory});
350404
return await configureThisProfile(profile, {app, customPrefs});
351405
}
352406

tests/unit/test-firefox/test.firefox.js

Lines changed: 210 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
fake,
1818
makeSureItFails,
1919
TCPConnectError,
20+
ErrorWithCode,
2021
} from '../helpers';
2122
import {manifestWithoutApps} from '../test-util/test.manifest';
2223
import {RemoteFirefox} from '../../../src/firefox/remote';
@@ -511,40 +512,240 @@ describe('firefox', () => {
511512
} catch (error) {
512513
exception = error;
513514
}
514-
515+
assert.instanceOf(exception, UsageError);
515516
assert.match(
516517
exception && exception.message,
517518
/Cannot use --keep-profile-changes on a default profile/
518519
);
519520
});
520521

522+
it('rejects to a UsageError when profile is not found',
523+
async () => {
524+
const fakeGetProfilePath = sinon.spy(() => Promise.resolve(false));
525+
const createProfileFinder = () => {
526+
return fakeGetProfilePath;
527+
};
528+
const isFirefoxDefaultProfile = sinon.spy(
529+
() => Promise.resolve(false)
530+
);
531+
532+
const promise = firefox.useProfile('profileName', {
533+
createProfileFinder,
534+
isFirefoxDefaultProfile,
535+
});
536+
537+
await assert.isRejected(promise, UsageError);
538+
await assert.isRejected(
539+
promise,
540+
/The request "profileName" profile name cannot be resolved/
541+
);
542+
}
543+
);
544+
521545
it('resolves to a FirefoxProfile instance', () => withBaseProfile(
522-
(baseProfile) => {
546+
async (baseProfile) => {
523547
const configureThisProfile = (profile) => Promise.resolve(profile);
524-
return firefox.useProfile(baseProfile.path(), {configureThisProfile})
525-
.then((profile) => {
526-
assert.instanceOf(profile, FirefoxProfile);
527-
});
548+
const createProfileFinder = () => {
549+
return (profilePath) => Promise.resolve(profilePath);
550+
};
551+
const profile = await firefox.useProfile(baseProfile.path(), {
552+
configureThisProfile,
553+
createProfileFinder,
554+
});
555+
assert.instanceOf(profile, FirefoxProfile);
556+
}
557+
));
558+
559+
it('looks for profile path if passed a name', () => withBaseProfile(
560+
async (baseProfile) => {
561+
const fakeGetProfilePath = sinon.spy(() => baseProfile.path());
562+
const createProfileFinder = () => {
563+
return fakeGetProfilePath;
564+
};
565+
const isFirefoxDefaultProfile = sinon.spy(
566+
() => Promise.resolve(false)
567+
);
568+
await firefox.useProfile('profileName', {
569+
createProfileFinder,
570+
isFirefoxDefaultProfile,
571+
});
572+
sinon.assert.calledOnce(fakeGetProfilePath);
573+
sinon.assert.calledWith(
574+
fakeGetProfilePath,
575+
sinon.match('profileName')
576+
);
577+
}
578+
));
579+
580+
it('checks if named profile is default', () => withBaseProfile(
581+
async (baseProfile) => {
582+
const createProfileFinder = () => {
583+
return () => Promise.resolve(baseProfile.path());
584+
};
585+
const isFirefoxDefaultProfile = sinon.spy(
586+
() => Promise.resolve(false)
587+
);
588+
await firefox.useProfile('profileName', {
589+
createProfileFinder,
590+
isFirefoxDefaultProfile,
591+
});
592+
sinon.assert.calledOnce(isFirefoxDefaultProfile);
593+
sinon.assert.calledWith(
594+
isFirefoxDefaultProfile,
595+
sinon.match('profileName')
596+
);
528597
}
529598
));
530599

531600
it('configures a profile', () => withBaseProfile(
532601
(baseProfile) => {
533602
const configureThisProfile =
534603
sinon.spy((profile) => Promise.resolve(profile));
535-
const app = 'fennec';
536604
const profilePath = baseProfile.path();
537-
return firefox.useProfile(profilePath, {app, configureThisProfile})
605+
return firefox.useProfile(profilePath, {configureThisProfile})
538606
.then((profile) => {
539607
sinon.assert.called(configureThisProfile);
540608
sinon.assert.calledWith(configureThisProfile, profile);
541-
assert.equal(configureThisProfile.firstCall.args[1].app, app);
542609
});
543610
}
544611
));
545612

546613
});
547614

615+
describe('defaultCreateProfileFinder', () => {
616+
617+
function prepareReaderTest(readProfileReturns) {
618+
const fakeReadProfiles = sinon.spy(() => {
619+
return readProfileReturns;
620+
});
621+
622+
const fakeGetPath = sinon.spy(() => Promise.resolve());
623+
const fakeProfiles = [{Name: 'someName'}];
624+
const userDirectoryPath = '/non/existent/path';
625+
626+
const FxProfile = {
627+
Finder() {
628+
return {
629+
readProfiles: fakeReadProfiles,
630+
getPath: fakeGetPath,
631+
profiles: fakeProfiles,
632+
};
633+
},
634+
};
635+
636+
return {
637+
fakeReadProfiles,
638+
fakeGetPath,
639+
fakeProfiles,
640+
FxProfile,
641+
userDirectoryPath,
642+
};
643+
}
644+
645+
it('creates a finder', async () => {
646+
const FxProfile = {
647+
Finder: sinon.spy(function() {
648+
return {};
649+
}),
650+
};
651+
firefox.defaultCreateProfileFinder({FxProfile});
652+
sinon.assert.calledWith(FxProfile.Finder, sinon.match(undefined));
653+
});
654+
655+
it('creates finder based on userDirectoryPath if present', async () => {
656+
const FxProfile = {
657+
Finder: sinon.spy(function() {
658+
return {};
659+
}),
660+
};
661+
const userDirectoryPath = '/non/existent/path';
662+
firefox.defaultCreateProfileFinder({userDirectoryPath, FxProfile});
663+
664+
sinon.assert.called(FxProfile.Finder);
665+
sinon.assert.calledWith(
666+
FxProfile.Finder,
667+
sinon.match(userDirectoryPath),
668+
);
669+
});
670+
671+
it('returns a finder that resolves a profile name', async () => {
672+
const {
673+
fakeReadProfiles,
674+
fakeGetPath,
675+
FxProfile,
676+
userDirectoryPath,
677+
} = prepareReaderTest(Promise.resolve());
678+
679+
const getter = firefox.defaultCreateProfileFinder({
680+
userDirectoryPath,
681+
FxProfile,
682+
});
683+
684+
await getter('someName');
685+
686+
sinon.assert.called(fakeReadProfiles);
687+
sinon.assert.called(fakeGetPath);
688+
sinon.assert.calledWith(
689+
fakeGetPath,
690+
sinon.match('someName'),
691+
);
692+
});
693+
694+
it('returns a finder that resolves undefined for no profiles.ini',
695+
async () => {
696+
const {
697+
fakeReadProfiles,
698+
fakeGetPath,
699+
FxProfile,
700+
userDirectoryPath,
701+
} = prepareReaderTest(
702+
Promise.reject(new ErrorWithCode('ENOENT', 'fake ENOENT error'))
703+
);
704+
705+
const getter = firefox.defaultCreateProfileFinder({
706+
userDirectoryPath,
707+
FxProfile,
708+
});
709+
710+
const res = await getter('someName');
711+
assert.equal(
712+
res,
713+
undefined,
714+
'Got an undefined result when the profiles.ini file does not exist');
715+
716+
sinon.assert.called(fakeReadProfiles);
717+
sinon.assert.notCalled(fakeGetPath);
718+
});
719+
720+
it('returns a finder that throws unexpected errors',
721+
async () => {
722+
const {
723+
fakeReadProfiles,
724+
fakeGetPath,
725+
FxProfile,
726+
userDirectoryPath,
727+
} = prepareReaderTest(
728+
Promise.reject(new Error('unspecified error'))
729+
);
730+
731+
const getter = firefox.defaultCreateProfileFinder({
732+
userDirectoryPath,
733+
FxProfile,
734+
});
735+
736+
const promise = getter('someName');
737+
738+
assert.isRejected(
739+
promise,
740+
'unspecified error',
741+
'Throws expected error'
742+
);
743+
sinon.assert.called(fakeReadProfiles);
744+
sinon.assert.notCalled(fakeGetPath);
745+
});
746+
747+
});
748+
548749
describe('configureProfile', () => {
549750

550751
function withTempProfile(callback) {

0 commit comments

Comments
 (0)