diff --git a/apps/files_sharing/js/external.js b/apps/files_sharing/js/external.js
index aeb4b2461f825d9597ca6f010fa6fc21c9fcc975..31407f28ffd2d7b06ba84f2b78a66b55b5effbc1 100644
--- a/apps/files_sharing/js/external.js
+++ b/apps/files_sharing/js/external.js
@@ -44,7 +44,8 @@
 					{name: name, owner: owner, remote: remoteClean}
 				),
 				t('files_sharing','Remote share'),
-				function (result) {
+				function (result, password) {
+					share.password = password;
 					callback(result, share);
 				},
 				true,
@@ -62,72 +63,92 @@
 		$buttons.eq(0).text(t('core', 'Cancel'));
 		$buttons.eq(1).text(t('files_sharing', 'Add remote share'));
 	};
-})();
 
-$(document).ready(function () {
-	// FIXME: HACK: do not init when running unit tests, need a better way
-	if (!window.TESTING && OCA.Files) {// only run in the files app
-		var params = OC.Util.History.parseUrlQuery();
+	OCA.Sharing.ExternalShareDialogPlugin = {
 
-		//manually add server-to-server share
-		if (params.remote && params.token && params.owner && params.name) {
+		filesApp: null,
 
-			var callbackAddShare = function(result, share) {
-				var password = share.password || '';
-				if (result) {
-					//$.post(OC.generateUrl('/apps/files_sharing/api/externalShares'), {id: share.id});
-					$.post(OC.generateUrl('apps/files_sharing/external'), {
-						remote: share.remote,
-						token: share.token,
-						owner: share.owner,
-						name: share.name,
-						password: password}, function(result) {
-						if (result.status === 'error') {
-							OC.Notification.show(result.data.message);
-						} else {
-							FileList.reload();
-						}
-					});
-				}
-			};
+		attach: function(filesApp) {
+			this.filesApp = filesApp;
+			this.processIncomingShareFromUrl();
+			this.processSharesToConfirm();
+		},
 
-			// clear hash, it is unlikely that it contain any extra parameters
-			location.hash = '';
-			params.passwordProtected = parseInt(params.protected, 10) === 1;
-			OCA.Sharing.showAddExternalDialog(
-				params,
-				params.passwordProtected,
-				callbackAddShare
-			);
-		}
+		/**
+		 * Process incoming remote share that might have been passed
+		 * through the URL
+		 */
+		processIncomingShareFromUrl: function() {
+			var fileList = this.filesApp.fileList;
+			var params = OC.Util.History.parseUrlQuery();
+			//manually add server-to-server share
+			if (params.remote && params.token && params.owner && params.name) {
 
-		// check for new server-to-server shares which need to be approved
-		$.get(OC.generateUrl('/apps/files_sharing/api/externalShares'),
-		{},
-		function(shares) {
-			var index;
-			for (index = 0; index < shares.length; ++index) {
-				OCA.Sharing.showAddExternalDialog(
-						shares[index],
-						false,
-						function(result, share) {
-							if (result) {
-								// Accept
-								$.post(OC.generateUrl('/apps/files_sharing/api/externalShares'), {id: share.id});
-								FileList.reload();
+				var callbackAddShare = function(result, share) {
+					var password = share.password || '';
+					if (result) {
+						//$.post(OC.generateUrl('/apps/files_sharing/api/externalShares'), {id: share.id});
+						$.post(OC.generateUrl('apps/files_sharing/external'), {
+							remote: share.remote,
+							token: share.token,
+							owner: share.owner,
+							name: share.name,
+							password: password}, function(result) {
+							if (result.status === 'error') {
+								OC.Notification.show(result.data.message);
 							} else {
-								// Delete
-								$.ajax({
-									url: OC.generateUrl('/apps/files_sharing/api/externalShares/'+share.id),
-									type: 'DELETE'
-								});
+								fileList.reload();
 							}
-						}
+						});
+					}
+				};
+
+				// clear hash, it is unlikely that it contain any extra parameters
+				location.hash = '';
+				params.passwordProtected = parseInt(params.protected, 10) === 1;
+				OCA.Sharing.showAddExternalDialog(
+					params,
+					params.passwordProtected,
+					callbackAddShare
 				);
 			}
+		},
 
-		});
+		/**
+		 * Retrieve a list of remote shares that need to be approved
+		 */
+		processSharesToConfirm: function() {
+			var fileList = this.filesApp.fileList;
+			// check for new server-to-server shares which need to be approved
+			$.get(OC.generateUrl('/apps/files_sharing/api/externalShares'),
+			{},
+			function(shares) {
+				var index;
+				for (index = 0; index < shares.length; ++index) {
+					OCA.Sharing.showAddExternalDialog(
+							shares[index],
+							false,
+							function(result, share) {
+								if (result) {
+									// Accept
+									$.post(OC.generateUrl('/apps/files_sharing/api/externalShares'), {id: share.id});
+									fileList.reload();
+								} else {
+									// Delete
+									$.ajax({
+										url: OC.generateUrl('/apps/files_sharing/api/externalShares/'+share.id),
+										type: 'DELETE'
+									});
+								}
+							}
+					);
+				}
+
+			});
+
+		}
+	};
+})();
 
-	}
+OC.Plugins.register('OCA.Files.App', OCA.Sharing.ExternalShareDialogPlugin);
 
-});
diff --git a/apps/files_sharing/tests/js/externalSpec.js b/apps/files_sharing/tests/js/externalSpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2f8f4508d4653429a8ab8edc8c7769c835d47eec
--- /dev/null
+++ b/apps/files_sharing/tests/js/externalSpec.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+describe('OCA.Sharing external tests', function() {
+	var plugin;
+	var urlQueryStub;
+	var promptDialogStub;
+	var confirmDialogStub;
+
+	function dummyShowDialog() {
+		var deferred = $.Deferred();
+		deferred.resolve();
+		return deferred.promise();
+	}
+
+	beforeEach(function() {
+		plugin = OCA.Sharing.ExternalShareDialogPlugin;
+		urlQueryStub = sinon.stub(OC.Util.History, 'parseUrlQuery');
+
+		confirmDialogStub = sinon.stub(OC.dialogs, 'confirm', dummyShowDialog);
+		promptDialogStub = sinon.stub(OC.dialogs, 'prompt', dummyShowDialog);
+
+		plugin.filesApp = {
+			fileList: {
+				reload: sinon.stub()
+			}
+		}
+	});
+	afterEach(function() {
+		urlQueryStub.restore();
+		confirmDialogStub.restore();
+		promptDialogStub.restore();
+		plugin = null;
+	});
+	describe('confirmation dialog from URL', function() {
+		var testShare;
+
+		/**
+		 * Checks that the server call's query matches what is
+		 * expected.
+		 *
+		 * @param {Object} expectedQuery expected query params
+		 */
+		function checkRequest(expectedQuery) {
+			var request = fakeServer.requests[0];
+			var query = OC.parseQueryString(request.requestBody);
+			expect(request.method).toEqual('POST');
+			expect(query).toEqual(expectedQuery);
+
+			request.respond(
+				200,
+				{'Content-Type': 'application/json'},
+				JSON.stringify({status: 'success'})
+			);
+			expect(plugin.filesApp.fileList.reload.calledOnce).toEqual(true);
+		}
+
+		beforeEach(function() {
+			testShare = {
+				remote: 'http://example.com/owncloud',
+				token: 'abcdefg',
+				owner: 'theowner',
+				name: 'the share name'
+			};
+		});
+		it('does nothing when no share was passed in URL', function() {
+			urlQueryStub.returns({});
+			plugin.processIncomingShareFromUrl();
+			expect(promptDialogStub.notCalled).toEqual(true);
+			expect(confirmDialogStub.notCalled).toEqual(true);
+			expect(fakeServer.requests.length).toEqual(0);
+		});
+		it('sends share info to server on confirm', function() {
+			urlQueryStub.returns(testShare);
+			plugin.processIncomingShareFromUrl();
+			expect(promptDialogStub.notCalled).toEqual(true);
+			expect(confirmDialogStub.calledOnce).toEqual(true);
+			confirmDialogStub.getCall(0).args[2](true);
+			expect(fakeServer.requests.length).toEqual(1);
+			checkRequest({
+				remote: 'http://example.com/owncloud',
+				token: 'abcdefg',
+				owner: 'theowner',
+				name: 'the share name',
+				password: ''
+			});
+		});
+		it('sends share info with password to server on confirm', function() {
+			testShare = _.extend(testShare, {protected: 1});
+			urlQueryStub.returns(testShare);
+			plugin.processIncomingShareFromUrl();
+			expect(promptDialogStub.calledOnce).toEqual(true);
+			expect(confirmDialogStub.notCalled).toEqual(true);
+			promptDialogStub.getCall(0).args[2](true, 'thepassword');
+			expect(fakeServer.requests.length).toEqual(1);
+			checkRequest({
+				remote: 'http://example.com/owncloud',
+				token: 'abcdefg',
+				owner: 'theowner',
+				name: 'the share name',
+				password: 'thepassword'
+			});
+		});
+		it('does not send share info on cancel', function() {
+			urlQueryStub.returns(testShare);
+			plugin.processIncomingShareFromUrl();
+			expect(promptDialogStub.notCalled).toEqual(true);
+			expect(confirmDialogStub.calledOnce).toEqual(true);
+			confirmDialogStub.getCall(0).args[2](false);
+			expect(fakeServer.requests.length).toEqual(0);
+		});
+	});
+	describe('show dialog for each share to confirm', function() {
+		// TODO test plugin.processSharesToConfirm()
+	});
+});
diff --git a/tests/karma.config.js b/tests/karma.config.js
index 414f15525846b053b507842c161934904f6101d6..e4f8b3ea68fddae9633c1f684bbb98b2a416a56f 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -53,7 +53,8 @@ module.exports = function(config) {
 					// up with the global namespace/classes/state
 					'apps/files_sharing/js/app.js',
 					'apps/files_sharing/js/sharedfilelist.js',
-					'apps/files_sharing/js/share.js'
+					'apps/files_sharing/js/share.js',
+					'apps/files_sharing/js/external.js'
 				],
 				testFiles: ['apps/files_sharing/tests/js/*.js']
 			},