Bug 1074664: Implement a non-persistent rooms store, r=mikedeboer

https://bugzilla.mozilla.org/show_bug.cgi?id=1074664
bit-rot fix copy
This commit is contained in:
Paul Kerr 2014-10-23 14:16:46 -07:00
parent 4d310e0d80
commit e95d4816c9
11 changed files with 368 additions and 48 deletions

View File

@ -203,7 +203,7 @@ let LoopCallsInternal = {
// Make the call to get the GUEST session regardless of whether the FXA
// request fails.
if (channelID == LoopCalls.channelIDs.FxA && MozLoopService.userProfile) {
if (channelID == MozLoopService.channelIDs.callsFxA && MozLoopService.userProfile) {
this._getCalls(LOOP_SESSION_TYPE.FXA, version);
} else {
this._getCalls(LOOP_SESSION_TYPE.GUEST, version);
@ -320,12 +320,6 @@ Object.freeze(LoopCallsInternal);
* Public API
*/
this.LoopCalls = {
// Channel ids that will be registered with the PushServer for notifications
channelIDs: {
FxA: "25389583-921f-4169-a426-a4673658944b",
Guest: "801f754b-686b-43ec-bd83-1419bbf58388",
},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.

View File

@ -5,21 +5,166 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["LoopRooms"];
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
/**
* Public Loop Rooms API
*/
this.LoopRooms = Object.freeze({
// Channel ids that will be registered with the PushServer for notifications
channelIDs: {
FxA: "6add272a-d316-477c-8335-f00f73dfde71",
Guest: "19d3f799-a8f3-4328-9822-b7cd02765832",
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
let consoleOptions = {
maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
prefix: "Loop",
};
return new ConsoleAPI(consoleOptions);
});
this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
let gRoomsListFetched = false;
let gRooms = new Map();
/**
* Callback used to indicate changes to rooms data on the LoopServer.
*
* @param {Object} version Version number assigned to this change set.
* @param {Object} channelID Notification channel identifier.
*
*/
const roomsPushNotification = function(version, channelID) {
return LoopRoomsInternal.onNotification(version, channelID);
};
let LoopRoomsInternal = {
getAll: function(callback) {
Task.spawn(function*() {
yield MozLoopService.register();
if (gRoomsListFetched) {
callback(null, [...gRooms.values()]);
return;
}
// Fetch the rooms from the server.
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
LOOP_SESSION_TYPE.GUEST;
let rooms = yield this.requestRoomList(sessionType);
// Add each room to our in-memory Map using a locally unique
// identifier.
for (let room of rooms) {
let id = MozLoopService.generateLocalID();
room.localRoomID = id;
// Next, request the detailed information for each room.
// If the request fails the room data will not be added to the map.
try {
let details = yield this.requestRoomDetails(room.roomToken, sessionType);
for (let attr in details) {
room[attr] = details[attr]
}
gRooms.set(id, room);
}
catch (error) {log.warn("failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
}
callback(null, [...gRooms.values()]);
return;
}.bind(this)).catch((error) => {log.error("getAll error:", error);
callback(error)});
return;
},
getRoomData: function(roomID, callback) {
if (gRooms.has(roomID)) {
callback(null, gRooms.get(roomID));
} else {
callback(new Error("Room data not found or not fetched yet for room with ID " + roomID));
}
return;
},
/**
* Request list of all rooms associated with this account.
*
* @param {String} sessionType Indicates which hawkRequest endpoint to use.
*
* @returns {Promise} room list
*/
requestRoomList: function(sessionType) {
return MozLoopService.hawkRequest(sessionType, "/rooms", "GET")
.then(response => {
let roomsList = JSON.parse(response.body);
if (!Array.isArray(roomsList)) {
// Force a reject in the returned promise.
// To be caught by the caller using the returned Promise.
throw new Error("Missing array of rooms in response.");
}
return roomsList;
});
},
/**
* Request information about a specific room from the server.
*
* @param {Object} token Room identifier returned from the LoopServer.
* @param {String} sessionType Indicates which hawkRequest endpoint to use.
*
* @returns {Promise} room details
*/
requestRoomDetails: function(token, sessionType) {
return MozLoopService.hawkRequest(sessionType, "/rooms/" + token, "GET")
.then(response => JSON.parse(response.body));
},
/**
* Callback used to indicate changes to rooms data on the LoopServer.
*
* @param {Object} version Version number assigned to this change set.
* @param {Object} channelID Notification channel identifier.
*
*/
onNotification: function(version, channelID) {
return;
},
});
};
Object.freeze(LoopRoomsInternal);
/**
* The LoopRooms class.
*
* Each method that is a member of this class requires the last argument to be a
* callback Function. MozLoopAPI will cause things to break if this invariant is
* violated. You'll notice this as well in the documentation for each method.
*/
this.LoopRooms = {
/**
* Fetch a list of rooms that the currently registered user is a member of.
*
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`. The second argument will
* be the list of rooms, if it was fetched successfully.
*/
getAll: function(callback) {
return LoopRoomsInternal.getAll(callback);
},
/**
* Return the current stored version of the data for the indicated room.
*
* @param {String} roomID Local room identifier
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`. The second argument will
* be the list of rooms, if it was fetched successfully.
*/
getRoomData: function(roomID, callback) {
return LoopRoomsInternal.getRoomData(roomID, callback);
},
};
Object.freeze(LoopRooms);

View File

@ -11,6 +11,8 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/loop/LoopCalls.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
Cu.import("resource:///modules/loop/LoopRooms.jsm");
Cu.import("resource:///modules/loop/LoopContacts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
@ -124,6 +126,7 @@ function injectLoopAPI(targetWindow) {
let ringerStopper;
let appVersionInfo;
let contactsAPI;
let roomsAPI;
let api = {
/**
@ -245,6 +248,21 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Returns the rooms API.
*
* @returns {Object} The rooms API object
*/
rooms: {
enumerable: true,
get: function() {
if (roomsAPI) {
return roomsAPI;
}
return roomsAPI = injectObjectAPI(LoopRooms, targetWindow);
}
},
/**
* Import a list of (new) contacts from an external data source.
*

View File

@ -71,6 +71,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoopCalls",
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
"resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "roomsPushNotification",
"resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
@ -342,17 +345,18 @@ let MozLoopServiceInternal = {
let options = mockWebSocket ? {mockWebSocket: mockWebSocket} : {};
gPushHandler.initialize(options);
let callsRegGuest = registerForNotification(LoopCalls.channelIDs.Guest,
let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
LoopCalls.onNotification);
let roomsRegGuest = registerForNotification(LoopRooms.channelIDs.Guest,
LoopRooms.onNotification);
let callsRegFxA = registerForNotification(LoopCalls.channelIDs.FxA,
let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
roomsPushNotification);
let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
LoopCalls.onNotification);
let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
roomsPushNotification);
let roomsRegFxA = registerForNotification(LoopRooms.channelIDs.FxA,
LoopRooms.onNotification);
Promise.all([callsRegGuest, roomsRegGuest, callsRegFxA, roomsRegFxA])
.then((pushUrls) => {
return this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST,
@ -880,8 +884,8 @@ let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSock
let registeredPromise =
MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {
calls: gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA],
rooms: gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA]
calls: gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
rooms: gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA]
});
registeredPromise.then(() => {
deferredInitialization.resolve("initialized to logged-in status");
@ -905,6 +909,17 @@ let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSock
this.MozLoopService = {
_DNSService: gDNSService,
get channelIDs() {
// Channel ids that will be registered with the PushServer for notifications
return {
callsFxA: "25389583-921f-4169-a426-a4673658944b",
callsGuest: "801f754b-686b-43ec-bd83-1419bbf58388",
roomsFxA: "6add272a-d316-477c-8335-f00f73dfde71",
roomsGuest: "19d3f799-a8f3-4328-9822-b7cd02765832",
};
},
set initializeTimerFunc(value) {
gInitializeTimerFunc = value;
},
@ -1127,6 +1142,21 @@ this.MozLoopService = {
return uuidgen.generateUUID().toString();
},
/**
* Returns a new non-global id
*
* @param {Function} notUnique [optional] This function will be
* applied to test the generated id for uniqueness
* in the callers domain.
*/
generateLocalID: function(notUnique = ((id) => {return false})) {
do {
var id = Date.now().toString(36) + Math.floor((Math.random() * 4096)).toString(16);
}
while (notUnique(id));
return id;
},
/**
* Retrieves MozLoopService "do not disturb" value.
*
@ -1265,8 +1295,8 @@ this.MozLoopService = {
return tokenData;
}).then(tokenData => {
return gRegisteredDeferred.promise.then(Task.async(function*() {
let callsUrl = gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA],
roomsUrl = gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA];
let callsUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
roomsUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
if (callsUrl && roomsUrl) {
yield MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {calls: callsUrl, rooms: roomsUrl});
@ -1314,8 +1344,8 @@ this.MozLoopService = {
log.debug("logOutFromFxA");
let callsPushUrl, roomsPushUrl;
if (gPushHandler) {
callsPushUrl = gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA];
roomsPushUrl = gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA];
callsPushUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
roomsPushUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
}
try {
if (callsPushUrl) {

View File

@ -256,8 +256,8 @@ add_task(function* basicAuthorizationAndRegistration() {
// Normally the same pushUrl would be registered but we change it in the test
// to be able to check for success on the second registration.
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = "https://localhost/pushUrl/fxa-calls";
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = "https://localhost/pushUrl/fxa-rooms";
mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = "https://localhost/pushUrl/fxa-calls";
mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = "https://localhost/pushUrl/fxa-rooms";
statusChangedPromise = promiseObserverNotified("loop-status-changed");
yield loadLoopPanel({loopURL: BASE_URL, stayOnline: true});
@ -333,8 +333,8 @@ add_task(function* loginWithParams401() {
add_task(function* logoutWithIncorrectPushURL() {
yield resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = pushURL;
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = pushURL;
mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = pushURL;
mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
@ -343,7 +343,7 @@ add_task(function* logoutWithIncorrectPushURL() {
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, {calls: pushURL});
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = "http://www.example.com/invalid";
mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = "http://www.example.com/invalid";
let caught = false;
yield MozLoopService.logOutFromFxA().catch((error) => {
caught = true;
@ -357,7 +357,7 @@ add_task(function* logoutWithIncorrectPushURL() {
add_task(function* logoutWithNoPushURL() {
yield resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = pushURL;
mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
@ -366,8 +366,8 @@ add_task(function* logoutWithNoPushURL() {
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, {calls: pushURL});
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = null;
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = null;
mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = null;
mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = null;
yield MozLoopService.logOutFromFxA();
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);

View File

@ -10,6 +10,7 @@ Cu.import("resource://testing-common/httpd.js");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource:///modules/loop/LoopCalls.jsm");
Cu.import("resource:///modules/loop/LoopRooms.jsm");
const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",

View File

@ -35,7 +35,7 @@ add_test(function test_busy_2guest_calls() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
@ -58,8 +58,8 @@ add_test(function test_busy_1fxa_1guest_calls() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
@ -82,7 +82,7 @@ add_test(function test_busy_2fxa_calls() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
@ -105,8 +105,8 @@ add_test(function test_busy_1guest_1fxa_calls() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");

View File

@ -37,7 +37,7 @@ add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
opened = true;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
waitForCondition(function() opened).then(() => {
run_next_test();
@ -56,7 +56,7 @@ add_test(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
opened = true;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
do_timeout(500, function() {
do_check_false(opened, "should not open a chat window");

View File

@ -16,7 +16,7 @@ add_test(function test_openChatWindow_on_notification() {
opened = true;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
waitForCondition(function() opened).then(() => {
do_check_true(opened, "should open a chat window");

View File

@ -0,0 +1,131 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
Cu.import("resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
"resource:///modules/Chat.jsm");
let hasTheseProps = function(a, b) {
for (let prop in a) {
if (a[prop] != b[prop]) {
do_print("hasTheseProps fail: prop = " + prop);
return false;
}
}
return true;
}
let openChatOrig = Chat.open;
add_test(function test_getAllRooms() {
let roomList = [
{ roomToken: "_nxD4V4FflQ",
roomName: "First Room Name",
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
maxSize: 2,
currSize: 0,
ctime: 1405517546 },
{ roomToken: "QzBbvGmIZWU",
roomName: "Second Room Name",
roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU",
maxSize: 2,
currSize: 0,
ctime: 140551741 },
{ roomToken: "3jKS_Els9IU",
roomName: "Third Room Name",
roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
maxSize: 3,
clientMaxSize: 2,
currSize: 1,
ctime: 1405518241 }
]
let roomDetail = {
roomName: "First Room Name",
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
roomOwner: "Alexis",
maxSize: 2,
clientMaxSize: 2,
creationTime: 1405517546,
expiresAt: 1405534180,
participants: [
{ displayName: "Alexis", account: "alexis@example.com", roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
{ displayName: "Adam", roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
]
}
loopServer.registerPathHandler("/rooms", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.write(JSON.stringify(roomList));
response.processAsync();
response.finish();
});
let returnRoomDetails = function(response, roomName) {
roomDetail.roomName = roomName;
response.setStatusLine(null, 200, "OK");
response.write(JSON.stringify(roomDetail));
response.processAsync();
response.finish();
}
loopServer.registerPathHandler("/rooms/_nxD4V4FflQ", (request, response) => {
returnRoomDetails(response, "First Room Name");
});
loopServer.registerPathHandler("/rooms/QzBbvGmIZWU", (request, response) => {
returnRoomDetails(response, "Second Room Name");
});
loopServer.registerPathHandler("/rooms/3jKS_Els9IU", (request, response) => {
returnRoomDetails(response, "Third Room Name");
});
MozLoopService.register(mockPushHandler).then(() => {
LoopRooms.getAll((error, rooms) => {
do_check_false(error);
do_check_true(rooms);
do_check_eq(rooms.length, 3);
do_check_eq(rooms[0].roomName, "First Room Name");
do_check_eq(rooms[1].roomName, "Second Room Name");
do_check_eq(rooms[2].roomName, "Third Room Name");
let room = rooms[0];
do_check_true(room.localRoomID);
do_check_true(hasTheseProps(roomList[0], room));
delete roomDetail.roomName;
delete room.participants;
delete roomDetail.participants;
do_check_true(hasTheseProps(roomDetail, room));
LoopRooms.getRoomData(room.localRoomID, (error, roomData) => {
do_check_false(error);
do_check_true(hasTheseProps(room, roomData));
run_next_test();
});
});
});
});
function run_test()
{
setupFakeLoopServer();
mockPushHandler.registrationPushURL = kEndPointUrl;
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.processAsync();
response.finish();
});
do_register_cleanup(function() {
// Revert original Chat.open implementation
Chat.open = openChatOrig;
});
run_next_test();
}

View File

@ -21,3 +21,4 @@ skip-if = toolkit == 'gonk'
[test_loopservice_token_send.js]
[test_loopservice_token_validation.js]
[test_loopservice_busy.js]
[test_rooms_getdata.js]