message: Add unit tests for `bindService()`
  `bindService()` is a function exported by `manager_service` to bind the
  service onto a Restify server.  This adds unit tests for this function.
diff: // See the License for the specific language governing permissions and
  // limitations under the License.
  +import fetch from 'node-fetch';
  import * as net from 'net';
  +import * as restify from 'restify';
  import {InMemoryConfig, JsonConfig} from '../infrastructure/json_config';
  import {AccessKey, AccessKeyRepository, DataLimit} from '../model/access_key';
  import {ManagerMetrics} from './manager_metrics';
  -import {ShadowsocksManagerService} from './manager_service';
  +import {bindService, ShadowsocksManagerService} from './manager_service';
  import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks';
  import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key';
  import {ServerConfigJson} from './server_config';
  @@ -738,6 +740,76 @@ describe('ShadowsocksManagerService', () => {
  });
  });
  +describe('bindService', () => {
  + let server: restify.Server;
  + let service: ShadowsocksManagerService;
  + let url: URL;
  + const prefix = '/TestApiPrefix';
  +
  + const fakeResponse = {'foo': 'bar'};
  + const fakeHandler = async (req, res, next) => {
  + res.send(200, fakeResponse);
  + next();
  + };
  +
  + beforeEach(() => {
  + server = restify.createServer();
  + service = new ShadowsocksManagerServiceBuilder().build();
  + server.listen(0);
  + url = new URL(server.url);
  + });
  +
  + afterEach(() => {
  + server.close();
  + });
  +
  + it('basic routing', async () => {
  + spyOn(service, "renameServer").and.callFake(fakeHandler);
  + bindService(server, prefix, service);
  +
  + url.pathname = `${prefix}/name`;
  + const response = await fetch(url, {method: 'put'});
  + const body = await response.json();
  +
  + expect(body).toEqual(fakeResponse);
  + expect(service.renameServer).toHaveBeenCalled();
  + });
  +
  + it('parameterized routing', async () => {
  + spyOn(service, "removeAccessKeyDataLimit").and.callFake(fakeHandler);
  + bindService(server, prefix, service);
  +
  + url.pathname = `${prefix}/access-keys/fake-access-key-id/data-limit`;
  + const response = await fetch(url, {method: 'delete'});
  + const body = await response.json();
  +
  + expect(body).toEqual(fakeResponse);
  + expect(service.removeAccessKeyDataLimit).toHaveBeenCalled();
  + });
  +
  + // Verify that we have consistent 404 behavior for all inputs.
  + [
  + '/',
  + '/TestApiPre',
  + '/foo',
  + '/very-long-path-that-does-not-exist',
  + `${prefix}/does-not-exist`,
  + ].forEach(path => {
  + it(`404 (${path})`, async () => {
  + // Ensure no methods are called.
  + spyOnAllFunctions(service);
  + jasmine.setDefaultSpyStrategy(fail);
  + bindService(server, prefix, service);
  +
  + url.pathname = path;
  + const response = await fetch(url);
  + const body = await response.json();
  +
  + expect(body).toEqual({code: 'ResourceNotFound', message: `${path} does not exist`});
  + });
  + });
  +});
  +
  class ShadowsocksManagerServiceBuilder {
  private defaultServerName_ = 'default name';
  private serverConfig_: JsonConfig<ServerConfigJson> = null;
  