I have a serious issue here. My application relies on SignalR functionality, but because of that I am unable to write unit tests. I am new to testing frameworks and have used Jasmine only in simple cases. SignalR has proven to be too much of a challenge for me, but I need to understand how I can successfully test it.
This is my CommsApp.ts file [typescript]:
/// <reference path="References.ts" />
var commsAnimationsModule = angular.module('forge.communications.animations', ['ngAnimate']);
var commsDirectivesModule = angular.module('forge.communications.directives', []);
var commsServicesModule = angular.module('forge.communications.services', []);
var commsFiltersModule = angular.module('forge.communications.filters', []);
var commsApp = angular.module('forge.communications.CommsApp',
[
'ngRoute',
'ngAnimate',
'cfValidation',
'ui.bootstrap',
'forge.communications.animations',
'forge.communications.directives',
'forge.communications.services',
'forge.communications.filters',
'angularFileUpload',
'timeRelative'
]);
commsApp.config(function ($routeProvider: ng.route.IRouteProvider, $locationProvider: any) {
$locationProvider.html5Mode(true);
$routeProvider.
when('/scheduled-messages', {
templateUrl: '/forge/CommsApp/js/Controllers/ScheduledMessageList/ScheduledMessageList.html',
controller: 'ScheduledMessageListController'
}).
when('/geotriggered-messages', {
templateUrl: '/forge/CommsApp/js/Controllers/GeoMessageList/GeoMessageList.html',
controller: 'GeoMessageListController'
}).
when('/scheduled-message/create', {
templateUrl: '/forge/CommsApp/js/Controllers/CreateScheduledMessage/CreateScheduledMessage.html',
controller: 'CreateScheduledMessageController'
}).
when('/scheduled-message/edit/:id', {
templateUrl: '/forge/CommsApp/js/Controllers/EditScheduledMessage/EditScheduledMessage.html',
controller: 'EditScheduledMessageController'
}).
when('/geotriggered-message/create', {
templateUrl: '/forge/CommsApp/js/Controllers/CreateGeotriggeredMessage/CreateGeotriggeredMessage.html',
controller: 'CreateGeotriggeredMessageController'
}).
when('/geotriggered-message/edit/:id', {
templateUrl: '/forge/CommsApp/js/Controllers/EditGeotriggeredMessage/EditGeotriggeredMessage.html',
controller: 'EditGeotriggeredMessageController'
}).
otherwise({
redirectTo: '/scheduled-messages'
});
});
commsApp.run(function ($rootScope: ICommsRootScope, commsSignalrEventService: CommsSignalrEventService, commsMgmtHttpService: CommsMgmtHttpServiceClient) {
// set up the items on the root scope
$rootScope.SelectedLocale = 'en-ie';
$rootScope.ForgeApplicationKey = "9496B737-7AE2-4FBD-B271-A64160759177";
$rootScope.AppVersionString = "1.0.0";
$rootScope.SessionToken = getCookie("ForgeSessionToken");
commsSignalrEventService.initialize().done(() => {
// send any messages about a new user logging in to the application here.
});
// call this at app startup to pre-cache this data for the create and edit pages
commsMgmtHttpService.GetUpdateMessageEditorOptions();
});
function getCookie(name:string) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
This is the test file (I removed most of the code since it's not working anyway, I have been chasing my own tail here):
describe('forge.communications.CommsApp', function () {
beforeEach(module('forge.communications.CommsApp'));
var route, rootScope, proxy, commsSignalrEventService;
beforeEach(inject(function (_$route_, _$rootScope_, _commsSignalrEventService_) {
route = _$route_,
rootScope = _$rootScope_;
commsSignalrEventService = _commsSignalrEventService_;
}));
describe("should map routes to controllers and templates", function () {
it("/scheduled-messages route should be mapped to ScheduledMessageListController", function () {
expect(2).toEqual(2);
expect(route.routes['/scheduled-messages'].controller).toBe('ScheduledMessageListController');
});
});
});
This is CommsSignalREventService.ts file:
var servicesModule: ng.IModule = angular.module('forge.communications.services');
servicesModule.factory('commsSignalrEventService', function ($rootScope): CommsSignalrEventService {
return new CommsSignalrEventService($rootScope);
});
class CommsSignalrEventService {
private $rootScope: ng.IScope;
private proxy:any = null;
constructor($rootScope) {
this.$rootScope = $rootScope;
}
public initialize(): JQueryPromise<any>{
this.proxy = $.connection['communicationsCenterHub'];
console.log('proxy',this.proxy);
//scheduled messages
this.proxy.client.broadcastScheduledMessageCreatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-created', { messageId: messageId });
};
this.proxy.client.broadcastScheduledMessageUpdatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-updated', { messageId: messageId });
};
this.proxy.client.broadcastScheduledMessageStateChangedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-statechanged', { messageId: messageId });
};
this.proxy.client.broadcastScheduledMessageDeletedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-message-deleted', { messageId: messageId });
};
//geotriggered messages
this.proxy.client.broadcastGeoMessageCreatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-created', { messageId: messageId });
};
this.proxy.client.broadcastGeoMessageUpdatedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-updated', { messageId: messageId });
};
this.proxy.client.broadcastGeoMessageStateChangedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-statechanged', { messageId: messageId });
};
this.proxy.client.broadcastGeoMessageDeletedEvent = (messageId: number) => {
this.$rootScope.$broadcast('comms-geomessage-deleted', { messageId: messageId });
};
var promise = $.connection.hub.start();
promise.done(function () {
//console.log('comms signalr hub started');
});
return promise;
}
public RegisterScheduledMessageCreated(messageId: number): void{
this.proxy.server.registerScheduledMessageCreated(messageId);
}
public RegisterScheduledMessageUpdated(messageId: number): void {
this.proxy.server.registerScheduledMessageUpdated(messageId);
}
public RegisterScheduledMessageDeleted(messageId: number): void {
this.proxy.server.registerScheduledMessageDeleted(messageId);
}
public RegisterScheduledMessageStateChanged(messageId: number): void {
this.proxy.server.registerScheduledMessageStateChanged(messageId);
}
public RegisterGeoMessageCreated(messageId: number): void {
this.proxy.server.registerGeoMessageCreated(messageId);
}
public RegisterGeoMessageUpdated(messageId: number): void {
this.proxy.server.registerGeoMessageUpdated(messageId);
}
public RegisterGeoMessageDeleted(messageId: number): void {
this.proxy.server.registerGeoMessageDeleted(messageId);
}
public RegisterGeoMessageStateChanged(messageId: number): void {
this.proxy.server.registerGeoMessageStateChanged(messageId);
}
}
The error I am seeing constantly in the command line, whenever I run karma, is forge.communications.CommsApp encountered a declaration exception FAILED
TypeError: Cannot read property 'client' of undefined at CommsSignalREventService, meaning that the 'proxy' variable in CommsSignalREventService.ts file is undefined:
this.proxy = $.connection['communicationsCenterHub'];
console.log('proxy', this.proxy); //resolves to UNDEFINED in tests, works fine in the app
this.proxy.client.broadcastScheduledMessageCreatedEvent = function (messageId) {
_this.$rootScope.$broadcast('comms-message-created', { messageId: messageId });
};
I will appreciate any help, because the amount of time I have put trying to figure it out is beyond ridiculous at this stage.
I think your issue is that your actual app references JavaScript dynamically generated by SignalR at runtime. This script is generally found at "~/signalr/hubs" or "~/signalr/js".
This script is what creates the $.connection['communicationsCenterHub'] proxy for you. Without a reference to this script in your test, this proxy will be undefined.
I'm presuming that you are not running the SignalR server when you are running your Jasmine tests. If this is the case you have two options:
You can simply copy the script generated by SignalR at "~/signalr/hubs" to a file and reference that in your tests. You could manually do this by pointing your browser to the generated script URL and, but you would have to update this file anytime your hub changes. You can automate this by running signalr.exe ghp /path:[path to the .dll that contains your Hub class] as a post-build step which will generate this file for you.
You can just avoid using the generated proxy altogether. Look at the "Without the generated proxy" section of the SignalR JS API reference.
Related
I'm experiencing intermittent signalr connection problems
sometimes it fails, sometimes it doesn't...
Here is the setup...
I have a list of orders, each order has a unique signalr connection. Currently there are 230 orders on a single page. The purpose of having a signalr connection is so users can see any real time updates on each order (who is viewing, editing, etc). I've decided to have a separate connection for each order so that I don't have to manage the order that is currently being viewed, edited, etc. So far, for the orders that have successfully connected, the updates are correct and smooth.
Here is my list with a sample of another user viewing an order (a photo of that user is being shown)
Here is my code that connects to the signalr hubs
crimeassure.factory('hubProxy', ['$rootScope', function ($rootScope) {
function hubProxyFactory(hubName) {
var _hubConnection = $.hubConnection();
_hubConnection.logging = true;
var _hubProxy = _hubConnection.createHubProxy(hubName);
return {
on: function (eventName, callback, failCallback) {
_hubProxy.on(eventName, function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
})
},
invoke: function (methodName, data) {
_hubProxy.invoke(methodName, data)
.done(function (result) {
//$rootScope.$apply(function () {
// if (callback) {
// callback(result);
// }
//});
});
},
start: function (successCallback, failCallback) {
_hubConnection.start({ transport: 'webSockets' }).done(successCallback).fail(failCallback);
},
hubConnection: _hubConnection,
};
};
return hubProxyFactory;
}]);
crimeassure.directive('componentLiveUpdates', function () {
return {
restrict: 'E',
scope: {
componentId: '=',
},
templateUrl: '/scripts/templates/directive-templates/component-live-updates.html',
controllerAs: 'vm',
bindToController: true,
controller: ["$scope", "$rootScope", "appData", "hubProxy",
function componentLiveUpdates($scope, $rootScope, appData, hubProxy) {
var vm = (this);
var user = appData.getCurrentUser();
vm.componentActivity = [];
var reQueueHub = hubProxy('researcherExpressQueueHub');
var componentActivityChanged = function (component) {
if (component.ActivityValue === 'ComponentModalClose') {
var idx = vm.componentActivity.indexOf(component);
vm.componentActivity.splice(idx, 1);
}
if (component.ActivityValue === 'ComponentModalOpen') {
vm.componentActivity.push(component);
}
}
var successCallback = function () {
console.log('connected to signalR, connection ID =' + reQueueHub.hubConnection.id + '--' + vm.componentId);
reQueueHub.invoke('joinGroup', vm.componentId);
$rootScope.reQueueHubs.push({
ComponentId: vm.componentId,
Hub: reQueueHub
});
}
var failCallback = function (e) {
console.log('Error connecting to signalR = ' + vm.componentId);
console.log(e);
//startHubConnection();
}
var startHubConnection = function () {
reQueueHub.start(successCallback, failCallback);
}
var initialize = function () {
reQueueHub.on('updateComponentActivity', componentActivityChanged);
startHubConnection();
}
initialize();
}],
}
});
and here is my hub class
public class ResearcherExpressQueueHub : Hub
{
public void UpdateComponentActivity(ComponentItem item)
{
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public void ComponentModalOpen(ComponentItem item)
{
item.Activity = ComponentActivity.ComponentModalOpen;
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public void ComponentModalClose(ComponentItem item)
{
item.Activity = ComponentActivity.ComponentModalClose;
Clients.Group(item.ComponentId.ToString()).updateComponentActivity(item);
}
public Task JoinGroup(string componentId)
{
return Groups.Add(Context.ConnectionId, componentId);
}
public Task LeaveGroup(string componentId)
{
return Groups.Remove(Context.ConnectionId, componentId);
}
}
so my questions are,
Why am i experiencing a disconnect "WebSocket is closed before the connection is established"
Is my approach the best way to approach this type of requirement?
Use grouping mechanisme of signalr and NOT create multiple connections for your usecase!
There are limitations from IIS and also from browsers. Some browser have a limit of 4 or 5 paralell connections. You can test it by yourself by opening multiple different browsers.
Details about grouping:
Working with groups in signalr is really simple. Details you will find here: https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/working-with-groups
I have a problem when I try to log some data inside the function of webtorrent.
I want to log some values of this.client.add but I don't have access.
Some idea of what's going on here?
import Webtorrent from 'webtorrent';
class PlaylistController {
/** #ngInject */
constructor($http, $log) {
this.log = $log;
this.client = new Webtorrent();
$http
.get('app/playlist/playlist.json')
.then(response => {
this.Torrent = response.data;
});
}
addTorrent(magnetUri) {
this.log.log(magnetUri);
this.client.add(magnetUri, function (torrent) {
// Got torrent metadata!
this.log.log('Client is downloading:', torrent.infoHash);
torrent.files.forEach(file => {
this.log(file);
});
});
this.log.log('sda');
this.log.log(this.client);
}
}
export const playlist = {
templateUrl: "app/playlist/playlist.html",
controller: PlaylistController,
bindings: {
playlist: '<'
}
};
Another thing its I use yeoman for the scaffold of my app and its has JSLint with console.log forbidden and its said that you must use angular.$log, but the thing its I don't wanna change that, I wanna understand the problem here.
You either need to refer to this (the class) as another variable to use inside the function(torrent) function or use arrow functions so that this reference remains the class one.
Solution 1, using another variable to ref the class:
addTorrent(magnetUri) {
this.log.log(magnetUri);
var that = this;
this.client.add(magnetUri, function (torrent) {
// Got torrent metadata!
that.log.log('Client is downloading:', torrent.infoHash);
torrent.files.forEach(file => {
that.log(file);
});
});
this.log.log('sda');
this.log.log(this.client);
}
Solution 2, using arrow functions:
addTorrent(magnetUri) {
this.log.log(magnetUri);
this.client.add(magnetUri, torrent => {
// Got torrent metadata!
this.log.log('Client is downloading:', torrent.infoHash);
torrent.files.forEach(file => {
this.log(file);
});
});
this.log.log('sda');
this.log.log(this.client);
}
Let's say i have two or more hubs in my server application. My javascipt client (Angular SPA) initialy needs a connection to the first hub, and needs to subscribe to a method like this:
connection = $.hubConnection(appSettings.serverPath);
firstHubProxy = connection.createHubProxy('firstHub');
firstHubProxy('eventFromFirstHub', function () {
console.log('Method invokation from FirstHub');
});
connection.start().done(function (data) {
console.log("hub started");
});
Everything is working fine. Now a user of my Angular SPA may decide to put a widget on his page, which needs to subcribe to a method from the second hub:
secondHubProxy = connection.createHubProxy('firstHub');
secondHubProxy('eventFromSecondHub', function () {
console.log('Method invokation from SecondHub');
});
The method from the second hub is not working. I guess because it was created after connection.start().
My example is simplified, in my real appplication there will be 20+ hubs to which users may or may not subscribe by adding or removing widgets to their page.
As far as i can tell i have two options:
call connection.stop() and then connection.start(). Now both hub subscriptions are working. This just doesn't feel right, because on all hubs, the OnConnected() event fires, and my application will be starting and stopping all the time.
create hub proxy objects for all possible hubs, subscribe to a dummy
method on all possible hubs, so the application can subscibe to hub
methods later if desired. This also doesn't feel right, because i
need to create 20+ hub proxies, while i may need just a few of
those.
Is anybody aware of a pattern which i can use to accomplish this? Or am i missing something very simple here?
Personally I use #2. I have a hub service that subscribes to all client methods. Any of my other angular components then pull that hub service in and subscribe to its events as needed.
Here it is;
hub.js
(function () {
'use strict';
angular
.module('app')
.factory('hub', hub);
hub.$inject = ['$timeout'];
function hub($timeout) {
var connection = $.connection.myHubName;
var service = {
connect: connect,
server: connection.server,
states: { connecting: 0, connected: 1, reconnecting: 2, na: 3, disconnected: 4 },
state: 4
};
service = angular.extend(service, OnNotify());
activate();
return service;
function activate() {
connection.client.start = function (something) {
service.notify("start", something);
}
connection.client.anotherMethod = function (p) {
service.notify("anotherMethod", p);
}
// etc for all client methods
$.connection.hub.stateChanged(function (change) {
$timeout(function () { service.state = change.newState; });
if (change.state != service.states.connected) service.notify("disconnected");
console.log("con:", _.invert(service.states)[change.oldState], ">", _.invert(service.states)[change.newState]);
});
connect();
}
function connect() {
$.connection.hub.start({ transport: 'auto' });
}
}
})();
OnNotify
var OnNotify = function () {
var callbacks = {};
return {
on: on,
notify: notify
};
function on(name, callback) {
if (!callbacks[name])
callbacks[name] = [];
callbacks[name].push(callback);
};
function notify(name, param) {
angular.forEach(callbacks[name], function (callback) {
callback(param);
});
};
}
Then I can subscribe to things as needed, for example in a controller;
(function () {
'use strict';
angular
.module('app')
.controller('MyController', MyController);
MyController.$inject = ['hub'];
function MyController(hub) {
/* jshint validthis:true */
var vm = this;
vm.something = {};
hub.on('start', function (something) {
$timeout(function () {
console.log(something);
vm.something = something;
});
});
}
})();
I'm trying to work out the very basics of updating my database using a Web API Controller that is backed by a repository pattern. So far I have everything working POST, GET, DELETE (Create, Read, Delete). But I'm missing the Update.
Below is my angular code, I'm not going to post the Angular Views/Templates, but just know that they do bind and they work just fine. My problem is only on the Edit View, where I try to update using the vm.save function. My save function works fine on the Angular side, but I'm not sure what to do on the Web API & Repository side. You will see that my code to get this working is very basic bare bones. I have all of the code pages from my project in a gist here:
All Files in Gist
Just in case you want to see the big picture, otherwise I will just put here the few pages where I am having trouble getting the Edit/Update methods to work in using http.put with Angular Controller, Web API Controller & Repository.
WORKING Angular Edit Controller:
function editFavoriteController($http, $window, $routeParams) {
var vm = this;
var url = "/api/favorites/" + $routeParams.searchId;
$http.get(url)
.success(function (result) {
vm.search = result[0];
})
.error(function () {
alert('error/failed');
})
.then(function () {
//Nothing
});
vm.update = function (id) {
var updateUrl = "/api/favorites/" + id;
$http.put(updateUrl, vm.editFavorite)
.success(function (result) {
var editFavorite = result.data;
//TODO: merge with existing favorites
//alert("Thanks for your post");
})
.error(function () {
alert("Your broken, go fix yourself!");
})
.then(function () {
$window.location = "#/";
});
};
};
NOT WORKING Web API Controller
public HttpResponseMessage Put(int id,[FromBody]Search editFavorite)
{
if (_favRepo.EditFavorite(id, editFavorite) && _favRepo.Save())
{
return Request.CreateResponse(HttpStatusCode.Created, editFavorite);
}
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
NOT WORKING Repository
public bool EditFavorite(int id, Search editFavorite)
{
try
{
var search = _ctx.Search.FirstOrDefault(s => s.SearchId == id);
search(editFavorite).State = EntityState.Modified;
return true;
}
catch
{
var item = "";
}
}
WORKING Interface
bool EditFavorite(int id, Search newSearch);
Again, my only problems are figuring out what to do for the update in the WebAPI FavoritesController and FavoritesRepository. I have example of how I have done everything else in the Gist, so I'm hoping someone might be able to help me out. I'm just hitting a wall of what I know how to do in Web API.
Fixed Code:
public HttpResponseMessage Put(int id,[FromBody]Search editFavorite)
{
if (_favRepo.EditFavorite(id, editFavorite))
{
_favRepo.Save()
return Request.CreateResponse(HttpStatusCode.Created, editFavorite);
}
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
I am also posting code which should work fine for handling edit on server side using WEB API and Repository Pattern.
WebAPI Controller:
public HttpResponseMessage Put(int id,[FromBody]Search editFavorite)
{
if (!ModelState.IsValid || id != editFavorite.Id)
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
db.EditFavorite(editFavorite);
try
{
db.Save();
}
catch (DbUpdateConcurrencyException)
{
if (!db.SearchExists(id))
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
else
{
throw;
}
}
return Request.CreateResponse(HttpStatusCode.Created, editFavorite);
}
Repository Method:
public void EditFavorite(Search editFavorite)
{
db.Entry(editFavorite).State = EntityState.Modified;
}
public void Save()
{
db.SaveChanges();
}
public bool SearchExists(int id)
{
return db.Search.Count(e => e.Id == id) > 0;
}
Modify Interface:
void Save();
void EditFavorite(Search newSearch);
bool SearchExists(int id);
Edit:
I have made some changes so that only operations that are carried out on your db context is done in repository layer (Data Layer) and the error checking is done in the WEB API Controller.
Suggestion:
You should inherit IDisposable on the interface and implement it your repository class so that your entities are properly disposed...
public interface IFavoritesRepository : IDisposable
{
// code here
}
public class FavoritesRepository : IFavoritesRepository
{
// code here
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
db.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
When posting a form in my web app I perform a couple of checks in javascript before validating the form in the backend. The js validation is dependent upon I18 messages and images.
If this was a scala template I would of course use #Messages and #routes.Assets.at but I don't want to mix the two(scala template and .js file).
E.g I have this check in my js file where currently the image routes is hardcoded:
$("form input[type=submit]").click(function (e) {
e.preventDefault();
var email = $("#username");
var emailPattern = /^([0-9a-zA-Z]([-\.\w]*[0-9a-zA-Z])*#([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/;
if (email.val() == "") {
email.css("background-image", "url('/assets/images/general/input-row-red.jpg')");
return e.preventDefault();
} else {
email.css("background-image", "url(/images/general/inputTextBg.png)");
}
});
I have tried to prepare the js files with the messages they need like this:
.js file:
/* Prepare messages*/
var messages = "";
$.getJSON("/messages/source", {
"keys": "sms.form.login.hide,sms.form.login"},
function (data) {
messages = data.messages;
});
MessageSource controller:
object MessageSource extends Controller {
def getMessages(keys : String) = Action { request =>
if(keys.isEmpty) {
BadRequest(Json.obj("status" -> "KO", "message" -> "key plix!"))
}
else {
var js = Map.empty[String, String]
for (k <- keys.split(",")) {
js = js + (k -> Messages(k))
}
Ok(Json.obj("status" -> "OK", "messages" -> js))
}
}
}
But I don't feel that this is the best solution. I have looked at http://www.playframework.com/documentation/2.1.0/ScalaRouting but I can't figure it out.
Maybe you have some nice solution for me?
Maybe this way?
jsfile:
#scripts = {
<script type="text/javascript" src="#routes.Application.javascriptRoutes"></script>
<script type="text/javascript">
jsRoutes.controllers.Application.messages("admin.venue.1,admin.venue.2,admin.venue.3" ).ajax({
type: 'GET',
success: function (data) {
console.log(data.messages);
},
error: function () {
console.log("error");
}
});
</script>
}
Controller:
object Application extends Controller {
def javascriptRoutes = Action {
implicit request =>
import routes.javascript._
Ok(
Routes.javascriptRouter("jsRoutes")
(
routes.javascript.Application.messages
)
).as("text/javascript")
}
def messages(keys : String) = Action {
implicit request => {
val messages = keys.split(",").map { key =>
key -> Messages(key)
}.toMap
Ok(Json.obj("status" -> "OK", "messages" -> messages))
}
}
}
routes:
# Javascript routes
GET /javascriptRoutes controllers.Application.javascriptRoutes
GET /messages controllers.Application.messages(keys: String)