/
rpc.js
272 lines (243 loc) · 8.57 KB
/
rpc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
'use strict';
var _ = require('lodash');
var async = require('async');
var net = require('net');
var rewire = require('rewire');
var config = require('config');
var rpc = rewire('data/rpc');
var persMock = require('../../mock/pers');
var RC = require('data/RequestContext');
var rcMock = require('../../mock/RequestContext');
var GameObject = require('model/GameObject');
var gsjsBridge = require('model/gsjsBridge');
suite('rpc', function () {
var CONFIG = {net: {
gameServers: {
gs01: {host: '127.0.0.1', ports: [3000, 3001]},
},
rpc: {basePort: 17000, timeout: 10000},
}};
// fake client GS connection to make rpc module establish client
// connection to its own server:
var GSCONF_LOOPBACK = {
gsid: 'gs01-loopback', // different from actual GSID to trick rpc module
host: '127.0.0.1',
publicHost: '127.0.0.1',
};
var cfgBackup;
suiteSetup(function () {
cfgBackup = config.get();
config.reset();
});
suiteTeardown(function () {
config.init(false, cfgBackup, {});
});
suite('initialization and shutdown', function () {
test('works', function (done) {
config.init(true, CONFIG, {});
var client;
async.series([
// start up RPC server
rpc.__get__('initServer').bind(rpc),
rpc.__get__('initClient').bind(rpc, GSCONF_LOOPBACK),
function checkInitialized(cb) {
assert.deepEqual(Object.keys(rpc.__get__('clients')),
['gs01-loopback'], 'exactly one client endpoint initialized');
// multitransport-jsonrpc specific stuff:
client = rpc.__get__('clients')['gs01-loopback'];
assert.deepEqual(client.transport.tcpConfig,
{host: '127.0.0.1', port: 17000});
var server = rpc.__get__('server');
assert.strictEqual(Object.keys(server.transport.connections).length,
1, 'exactly one client connected to server');
var connection = server.transport.connections[Object.keys(
server.transport.connections)[0]];
assert.isTrue(connection.remoteAddress === '127.0.0.1' ||
connection.remoteAddress === '::ffff:127.0.0.1');
assert.strictEqual(connection.remotePort,
client.transport.con.localPort,
'server and client endpoints are connected');
return cb();
},
rpc.shutdown.bind(rpc),
], function (err) {
if (err) return done(err);
// test shutdown, too
assert.deepEqual(rpc.__get__('clients'), {});
// multitransport-jsonrpc specific stuff:
assert.isTrue(client.transport.con.destroyed);
return done();
});
});
});
suite('function calls', function () {
setup(function (done) {
rcMock.reset();
// enable mock persistence layer
rpc.__set__('pers', persMock);
persMock.reset();
// set up client/server loopback connection within the same process
// (as a worker process so it is managing the test game objects)
config.init(false, CONFIG, {gsid: 'gs01-01'});
rpc.__get__('initServer')(function serverStarted() {
// meddle with base port to get a loopback client in worker process
require('nconf').overrides({net: {rpc: {basePort: 17001}}});
rpc.__get__('initClient')(GSCONF_LOOPBACK, function clientStarted() {
done();
});
require('nconf').overrides({}); // reset
});
});
teardown(function (done) {
rcMock.reset();
persMock.reset();
rpc.__set__('pers', require('data/pers'));
rpc.shutdown(function callback() {
done();
});
});
test('dummy function', function (done) {
var server = rpc.__get__('server');
var client = rpc.__get__('clients')['gs01-loopback'];
server.register('dummyFunc', function (a, b, callback) {
callback(null, a + b);
});
client.register('dummyFunc');
client.dummyFunc(2, 3, function (err, result) {
assert.strictEqual(result, 5);
done(err);
});
});
test('function call on actual game object', function (done) {
// make fake client RPC connection available under our own GSID (so
// requests on objects we are managing go to our own RPC server):
rpc.__get__('clients')['gs01-02'] = rpc.__get__('clients')['gs01-loopback'];
// create dummy object with a test function (we're the only
// configured GS, so we are authoritative for it):
var go = new GameObject({
tsid: 'LXYZ',
foo: function (a, b) {
return a + b;
},
});
persMock.preAdd(go);
rcMock.run(function () {
var res = rpc.sendObjRequest(go, 'foo', [17, 4]);
assert.strictEqual(res, 21, 'function is actually called');
}, null, null, done);
});
test('return null if called function returns undefined', function (done) {
rpc.__get__('clients')['gs01-02'] = rpc.__get__('clients')['gs01-loopback'];
persMock.preAdd(new GameObject({
tsid: 'LXYZ',
func: _.noop,
}));
rcMock.run(function () {
var res = rpc.sendObjRequest('LXYZ', 'func', []);
assert.isNull(res, 'undefined (unknown in JSON) is converted to null');
}, null, null, done);
});
test('return proper RPC result object if called function returns undefined', function (done) {
persMock.preAdd(new GameObject({
tsid: 'LXYZ',
func: _.noop,
}));
var socket = net.connect({port: 17001}, function () {
socket.on('data', function (data) {
var length = data.readUInt32BE(0);
var s = data.toString('utf8', 4);
assert.strictEqual(s.length, length);
var res = JSON.parse(s);
assert.strictEqual(res.id, 123);
assert.isUndefined(res.error, 'no error occurred');
assert.isNull(res.result,
'response contains result property with value null');
done();
});
var msg = JSON.stringify({method: 'obj',
params: ['foo', 'LXYZ', null, 'func', []], id: 123});
var length = Buffer.byteLength(msg);
var buf = new Buffer(4 + length);
buf.writeUInt32BE(length, 0);
buf.write(msg, 4, length, 'utf8');
socket.write(buf);
});
});
test('handles invalid (non-array) args parameter gracefully', function (done) {
persMock.preAdd(new GameObject({
tsid: 'LX',
func: function () {
return 'foo';
},
}));
rcMock.run(function () {
rpc.__get__('objectRequest')('caller', 'LX', null, 'func', null, function cb(err, res) {
if (err) return done(err);
assert.strictEqual(res, 'foo');
done();
});
});
});
test('handles calls on nonexistent game objects gracefully', function (done) {
rcMock.run(function () {
rpc.__get__('objectRequest')('caller', 'IMISSING', null, 'func', null, function cb(err, res) {
assert.instanceOf(err, Error);
assert.strictEqual(err.message, 'object not found: IMISSING');
return done();
});
});
});
});
suite('model API function calls', function () {
setup(function (done) {
// set up client/server loopback connection within the same process
config.init(false, CONFIG, {gsid: 'gs01-01', gsjs: {config: 'config_prod'}});
gsjsBridge.init(true);
rpc.__get__('initServer')(function serverStarted() {
// meddle with base port to get a loopback client in worker process
require('nconf').overrides({net: {rpc: {basePort: 17001}}});
rpc.__get__('initClient')(GSCONF_LOOPBACK, function clientStarted() {
// make fake client RPC connection available under our own GSID
rpc.__get__('clients')['gs01-02'] =
rpc.__get__('clients')['gs01-loopback'];
done();
});
require('nconf').overrides({}); // reset
});
});
teardown(function (done) {
gsjsBridge.reset();
rpc.shutdown(function callback() {
done();
});
});
test('apiFindItemPrototype retrieves catalog objects properly', function (done) {
new RC().run(function () {
var res = rpc.sendRequest('gs01-02', 'api',
['apiFindItemPrototype', ['catalog']]);
assert.notProperty(res, 'objref', 'return value is not an objref');
assert.isObject(res, 'class_tsids');
}, done);
});
test('apiGetJSFileObject returns object prototypes (not objrefs)', function (done) {
new RC().run(function () {
var res = rpc.sendRequest('gs01-02', 'api',
['apiGetJSFileObject', ['achievements/able_chopper.js']]);
assert.notProperty(res, 'objref', 'return value is not an objref');
assert.strictEqual(res.category, 'cooking');
res = rpc.sendRequest('gs01-02', 'api',
['apiGetJSFileObject', ['items/apple.js']]);
assert.notProperty(res, 'objref', 'return value is not an objref');
assert.property(res.verbs, 'lick');
}, done);
});
test('apiFindQuestPrototype returns quest prototypes (not objrefs)', function (done) {
new RC().run(function () {
var res = rpc.sendRequest('gs01-02', 'api',
['apiFindQuestPrototype', ['an_autumn_day']]);
assert.notProperty(res, 'objref', 'return value is not an objref');
assert.strictEqual(res.title, 'An Autumn Day');
}, done);
});
});
});