github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/util/api.spec.ts (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import { assert } from "chai";
    12  import _ from "lodash";
    13  import moment from "moment";
    14  import Long from "long";
    15  
    16  import fetchMock from "./fetch-mock";
    17  
    18  import * as protos from "src/js/protos";
    19  import * as api from "./api";
    20  
    21  describe("rest api", function() {
    22    describe("propsToQueryString", function () {
    23      interface PropBag {
    24        [k: string]: string;
    25      }
    26  
    27      // helper decoding function used to doublecheck querystring generation
    28      function decodeQueryString(qs: string): PropBag {
    29        return _.reduce<string, PropBag>(
    30          qs.split("&"),
    31          (memo: PropBag, v: string) => {
    32            const [key, value] = v.split("=");
    33            memo[decodeURIComponent(key)] = decodeURIComponent(value);
    34            return memo;
    35          },
    36          {},
    37        );
    38      }
    39  
    40      it("creates an appropriate querystring", function () {
    41        const testValues: { [k: string]: any } = {
    42          a: "testa",
    43          b: "testb",
    44        };
    45  
    46        const querystring = api.propsToQueryString(testValues);
    47  
    48        assert((/a=testa/).test(querystring));
    49        assert((/b=testb/).test(querystring));
    50        assert.lengthOf(querystring.match(/=/g), 2);
    51        assert.lengthOf(querystring.match(/&/g), 1);
    52        assert.deepEqual(testValues, decodeQueryString(querystring));
    53      });
    54  
    55      it("handles falsy values correctly", function () {
    56        const testValues: { [k: string]: any } = {
    57          // null and undefined should be ignored
    58          undefined: undefined,
    59          null: null,
    60          // other values should be added
    61          false: false,
    62          "": "",
    63          0: 0,
    64        };
    65  
    66        const querystring = api.propsToQueryString(testValues);
    67  
    68        assert((/false=false/).test(querystring));
    69        assert((/0=0/).test(querystring));
    70        assert((/([^A-Za-z]|^)=([^A-Za-z]|$)/).test(querystring));
    71        assert.lengthOf(querystring.match(/=/g), 3);
    72        assert.lengthOf(querystring.match(/&/g), 2);
    73        assert.notOk((/undefined/).test(querystring));
    74        assert.notOk((/null/).test(querystring));
    75        assert.deepEqual({ false: "false", "": "", 0: "0" }, decodeQueryString(querystring));
    76      });
    77  
    78      it("handles special characters", function () {
    79        const key = "!@#$%^&*()=+-_\\|\"`'?/<>";
    80        const value = key.split("").reverse().join(""); // key reversed
    81        const testValues: { [k: string]: any } = {
    82          [key] : value,
    83        };
    84  
    85        const querystring = api.propsToQueryString(testValues);
    86  
    87        assert(querystring.match(/%/g).length > (key + value).match(/%/g).length);
    88        assert.deepEqual(testValues, decodeQueryString(querystring));
    89      });
    90  
    91      it("handles non-string values", function () {
    92        const testValues: { [k: string]: any } = {
    93          boolean: true,
    94          number: 1,
    95          emptyObject: {},
    96          emptyArray: [],
    97          objectWithProps: { a: 1, b: 2 },
    98          arrayWithElts: [1, 2, 3],
    99          long: Long.fromNumber(1),
   100        };
   101  
   102        const querystring = api.propsToQueryString(testValues);
   103        assert.deepEqual(_.mapValues(testValues, _.toString), decodeQueryString(querystring));
   104      });
   105    });
   106  
   107    describe("databases request", function () {
   108      afterEach(fetchMock.restore);
   109  
   110      it("correctly requests info about all databases", function () {
   111        this.timeout(1000);
   112        // Mock out the fetch query to /databases
   113        fetchMock.mock({
   114          matcher: api.API_PREFIX + "/databases",
   115          method: "GET",
   116          response: (_url: string, requestObj: RequestInit) => {
   117            assert.isUndefined(requestObj.body);
   118            const encodedResponse = protos.cockroach.server.serverpb.DatabasesResponse.encode({
   119              databases: ["system", "test"],
   120            }).finish();
   121            return {
   122              body: api.toArrayBuffer(encodedResponse),
   123            };
   124          },
   125        });
   126  
   127        return api.getDatabaseList(new protos.cockroach.server.serverpb.DatabasesRequest()).then((result) => {
   128          assert.lengthOf(fetchMock.calls(api.API_PREFIX + "/databases"), 1);
   129          assert.lengthOf(result.databases, 2);
   130        });
   131      });
   132  
   133      it("correctly handles an error", function (done) {
   134        this.timeout(1000);
   135        // Mock out the fetch query to /databases, but return a promise that's never resolved to test the timeout
   136        fetchMock.mock({
   137          matcher: api.API_PREFIX + "/databases",
   138          method: "GET",
   139          response: (_url: string, requestObj: RequestInit) => {
   140            assert.isUndefined(requestObj.body);
   141            return { throws: new Error() };
   142          },
   143        });
   144  
   145        api.getDatabaseList(new protos.cockroach.server.serverpb.DatabasesRequest()).then((_result) => {
   146          done(new Error("Request unexpectedly succeeded."));
   147        }).catch(function (e) {
   148          assert(_.isError(e));
   149          done();
   150        });
   151      });
   152  
   153      it("correctly times out", function (done) {
   154        this.timeout(1000);
   155        // Mock out the fetch query to /databases, but return a promise that's never resolved to test the timeout
   156        fetchMock.mock({
   157          matcher: api.API_PREFIX + "/databases",
   158          method: "GET",
   159          response: (_url: string, requestObj: RequestInit) => {
   160            assert.isUndefined(requestObj.body);
   161            return new Promise<any>(() => { });
   162          },
   163        });
   164  
   165        api.getDatabaseList(new protos.cockroach.server.serverpb.DatabasesRequest(), moment.duration(0)).then((_result) => {
   166          done(new Error("Request unexpectedly succeeded."));
   167        }).catch(function (e) {
   168          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   169          done();
   170        });
   171      });
   172    });
   173  
   174    describe("database details request", function () {
   175      const dbName = "test";
   176  
   177      afterEach(fetchMock.restore);
   178  
   179      it("correctly requests info about a specific database", function () {
   180        this.timeout(1000);
   181        // Mock out the fetch query
   182        fetchMock.mock({
   183          matcher: `${api.API_PREFIX}/databases/${dbName}`,
   184          method: "GET",
   185          response: (_url: string, requestObj: RequestInit) => {
   186            assert.isUndefined(requestObj.body);
   187            const encodedResponse = protos.cockroach.server.serverpb.DatabaseDetailsResponse.encode({
   188              table_names: ["table1", "table2"],
   189              grants: [
   190                { user: "root", privileges: ["ALL"] },
   191                { user: "other", privileges: [] },
   192              ],
   193            }).finish();
   194            return {
   195              body: api.toArrayBuffer(encodedResponse),
   196            };
   197          },
   198        });
   199  
   200        return api.getDatabaseDetails(new protos.cockroach.server.serverpb.DatabaseDetailsRequest({ database: dbName })).then((result) => {
   201          assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/databases/${dbName}`), 1);
   202          assert.lengthOf(result.table_names, 2);
   203          assert.lengthOf(result.grants, 2);
   204        });
   205      });
   206  
   207      it("correctly handles an error", function (done) {
   208        this.timeout(1000);
   209        // Mock out the fetch query, but return a 500 status code
   210        fetchMock.mock({
   211          matcher: `${api.API_PREFIX}/databases/${dbName}`,
   212          method: "GET",
   213          response: (_url: string, requestObj: RequestInit) => {
   214            assert.isUndefined(requestObj.body);
   215            return { throws: new Error() };
   216          },
   217        });
   218  
   219        api.getDatabaseDetails(new protos.cockroach.server.serverpb.DatabaseDetailsRequest({ database: dbName })).then((_result) => {
   220          done(new Error("Request unexpectedly succeeded."));
   221        }).catch(function (e) {
   222          assert(_.isError(e));
   223          done();
   224        });
   225      });
   226  
   227      it("correctly times out", function (done) {
   228        this.timeout(1000);
   229        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   230        fetchMock.mock({
   231          matcher: `${api.API_PREFIX}/databases/${dbName}`,
   232          method: "GET",
   233          response: (_url: string, requestObj: RequestInit) => {
   234            assert.isUndefined(requestObj.body);
   235            return new Promise<any>(() => { });
   236          },
   237        });
   238  
   239        api.getDatabaseDetails(new protos.cockroach.server.serverpb.DatabaseDetailsRequest({ database: dbName }), moment.duration(0)).then((_result) => {
   240          done(new Error("Request unexpectedly succeeded."));
   241        }).catch(function (e) {
   242          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   243          done();
   244        });
   245      });
   246    });
   247  
   248    describe("table details request", function () {
   249      const dbName = "testDB";
   250      const tableName = "testTable";
   251  
   252      afterEach(fetchMock.restore);
   253  
   254      it("correctly requests info about a specific table", function () {
   255        this.timeout(1000);
   256        // Mock out the fetch query
   257        fetchMock.mock({
   258          matcher: `${api.API_PREFIX}/databases/${dbName}/tables/${tableName}`,
   259          method: "GET",
   260          response: (_url: string, requestObj: RequestInit) => {
   261            assert.isUndefined(requestObj.body);
   262            const encodedResponse = protos.cockroach.server.serverpb.TableDetailsResponse.encode({}).finish();
   263            return {
   264              body: api.toArrayBuffer(encodedResponse),
   265            };
   266          },
   267        });
   268  
   269        return api.getTableDetails(new protos.cockroach.server.serverpb.TableDetailsRequest({ database: dbName, table: tableName })).then((result) => {
   270          assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/databases/${dbName}/tables/${tableName}`), 1);
   271          assert.lengthOf(result.columns, 0);
   272          assert.lengthOf(result.indexes, 0);
   273          assert.lengthOf(result.grants, 0);
   274        });
   275      });
   276  
   277      it("correctly handles an error", function (done) {
   278        this.timeout(1000);
   279        // Mock out the fetch query, but return a 500 status code
   280        fetchMock.mock({
   281          matcher: `${api.API_PREFIX}/databases/${dbName}/tables/${tableName}`,
   282          method: "GET",
   283          response: (_url: string, requestObj: RequestInit) => {
   284            assert.isUndefined(requestObj.body);
   285            return { throws: new Error() };
   286          },
   287        });
   288  
   289        api.getTableDetails(new protos.cockroach.server.serverpb.TableDetailsRequest({ database: dbName, table: tableName })).then((_result) => {
   290          done(new Error("Request unexpectedly succeeded."));
   291        }).catch(function (e) {
   292          assert(_.isError(e));
   293          done();
   294        });
   295      });
   296  
   297      it("correctly times out", function (done) {
   298        this.timeout(1000);
   299        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   300        fetchMock.mock({
   301          matcher: `${api.API_PREFIX}/databases/${dbName}/tables/${tableName}`,
   302          method: "GET",
   303          response: (_url: string, requestObj: RequestInit) => {
   304            assert.isUndefined(requestObj.body);
   305            return new Promise<any>(() => { });
   306          },
   307        });
   308  
   309        api.getTableDetails(new protos.cockroach.server.serverpb.TableDetailsRequest({ database: dbName, table: tableName }), moment.duration(0)).then((_result) => {
   310          done(new Error("Request unexpectedly succeeded."));
   311        }).catch(function (e) {
   312          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   313          done();
   314        });
   315      });
   316    });
   317  
   318    describe("events request", function() {
   319      const eventsPrefixMatcher = `begin:${api.API_PREFIX}/events?`;
   320  
   321      afterEach(fetchMock.restore);
   322  
   323      it("correctly requests events", function () {
   324        this.timeout(1000);
   325        // Mock out the fetch query
   326        fetchMock.mock({
   327          matcher: eventsPrefixMatcher,
   328          method: "GET",
   329          response: (_url: string, requestObj: RequestInit) => {
   330            assert.isUndefined(requestObj.body);
   331            const encodedResponse = protos.cockroach.server.serverpb.EventsResponse.encode({
   332              events: [
   333                { event_type: "test" },
   334              ],
   335            }).finish();
   336            return {
   337              body: api.toArrayBuffer(encodedResponse),
   338            };
   339          },
   340        });
   341  
   342        return api.getEvents(new protos.cockroach.server.serverpb.EventsRequest()).then((result) => {
   343          assert.lengthOf(fetchMock.calls(eventsPrefixMatcher), 1);
   344          assert.lengthOf(result.events, 1);
   345        });
   346      });
   347  
   348      it("correctly requests filtered events", function () {
   349        this.timeout(1000);
   350  
   351        const req = new protos.cockroach.server.serverpb.EventsRequest({
   352          target_id: Long.fromNumber(1),
   353          type: "test type",
   354        });
   355  
   356        // Mock out the fetch query
   357        fetchMock.mock({
   358          matcher: eventsPrefixMatcher,
   359          method: "GET",
   360          response: (url: string, requestObj: RequestInit) => {
   361            const params = url.split("?")[1].split("&");
   362            assert.lengthOf(params, 3);
   363            _.each(params, (param) => {
   364              let [k, v] = param.split("=");
   365              k = decodeURIComponent(k);
   366              v = decodeURIComponent(v);
   367              switch (k) {
   368                case "target_id":
   369                  assert.equal(req.target_id.toString(), v);
   370                  break;
   371  
   372                case "type":
   373                  assert.equal(req.type, v);
   374                  break;
   375  
   376                case "unredacted_events":
   377                  break;
   378  
   379                default:
   380                   throw new Error(`Unknown property ${k}`);
   381              }
   382            });
   383            assert.isUndefined(requestObj.body);
   384            const encodedResponse = protos.cockroach.server.serverpb.EventsResponse.encode({
   385              events: [
   386                { event_type: "test" },
   387              ],
   388            }).finish();
   389            return {
   390              body: api.toArrayBuffer(encodedResponse),
   391            };
   392          },
   393        });
   394  
   395        return api.getEvents(new protos.cockroach.server.serverpb.EventsRequest(req)).then((result) => {
   396          assert.lengthOf(fetchMock.calls(eventsPrefixMatcher), 1);
   397          assert.lengthOf(result.events, 1);
   398        });
   399      });
   400  
   401      it("correctly handles an error", function (done) {
   402        this.timeout(1000);
   403  
   404        // Mock out the fetch query, but return a 500 status code
   405        fetchMock.mock({
   406          matcher: eventsPrefixMatcher,
   407          method: "GET",
   408          response: (_url: string, requestObj: RequestInit) => {
   409            assert.isUndefined(requestObj.body);
   410            return { throws: new Error() };
   411          },
   412        });
   413  
   414        api.getEvents(new protos.cockroach.server.serverpb.EventsRequest()).then((_result) => {
   415          done(new Error("Request unexpectedly succeeded."));
   416        }).catch(function (e) {
   417          assert(_.isError(e));
   418          done();
   419        });
   420      });
   421  
   422      it("correctly times out", function (done) {
   423        this.timeout(1000);
   424        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   425        fetchMock.mock({
   426          matcher: eventsPrefixMatcher,
   427          method: "GET",
   428          response: (_url: string, requestObj: RequestInit) => {
   429            assert.isUndefined(requestObj.body);
   430            return new Promise<any>(() => { });
   431          },
   432        });
   433  
   434        api.getEvents(new protos.cockroach.server.serverpb.EventsRequest(), moment.duration(0)).then((_result) => {
   435          done(new Error("Request unexpectedly succeeded."));
   436        }).catch(function (e) {
   437          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   438          done();
   439        });
   440      });
   441    });
   442  
   443    describe("health request", function() {
   444      const healthUrl = `${api.API_PREFIX}/health`;
   445  
   446      afterEach(fetchMock.restore);
   447  
   448      it("correctly requests health", function () {
   449        this.timeout(1000);
   450        // Mock out the fetch query
   451        fetchMock.mock({
   452          matcher: healthUrl,
   453          method: "GET",
   454          response: (_url: string, requestObj: RequestInit) => {
   455            assert.isUndefined(requestObj.body);
   456            const encodedResponse = protos.cockroach.server.serverpb.HealthResponse.encode({}).finish();
   457            return {
   458              body: api.toArrayBuffer(encodedResponse),
   459            };
   460          },
   461        });
   462  
   463        return api.getHealth(new protos.cockroach.server.serverpb.HealthRequest()).then((result) => {
   464          assert.lengthOf(fetchMock.calls(healthUrl), 1);
   465          assert.deepEqual(result, new protos.cockroach.server.serverpb.HealthResponse());
   466        });
   467      });
   468  
   469      it("correctly handles an error", function (done) {
   470        this.timeout(1000);
   471  
   472        // Mock out the fetch query, but return a 500 status code
   473        fetchMock.mock({
   474          matcher: healthUrl,
   475          method: "GET",
   476          response: (_url: string, requestObj: RequestInit) => {
   477            assert.isUndefined(requestObj.body);
   478            return { throws: new Error() };
   479          },
   480        });
   481  
   482        api.getHealth(new protos.cockroach.server.serverpb.HealthRequest()).then((_result) => {
   483          done(new Error("Request unexpectedly succeeded."));
   484        }).catch(function (e) {
   485          assert(_.isError(e));
   486          done();
   487        });
   488      });
   489  
   490      it("correctly times out", function (done) {
   491        this.timeout(1000);
   492        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   493        fetchMock.mock({
   494          matcher: healthUrl,
   495          method: "GET",
   496          response: (_url: string, requestObj: RequestInit) => {
   497            assert.isUndefined(requestObj.body);
   498            return new Promise<any>(() => { });
   499          },
   500        });
   501  
   502        api.getHealth(new protos.cockroach.server.serverpb.HealthRequest(), moment.duration(0)).then((_result) => {
   503          done(new Error("Request unexpectedly succeeded."));
   504        }).catch(function (e) {
   505          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   506          done();
   507        });
   508      });
   509    });
   510  
   511    describe("cluster request", function() {
   512      const clusterUrl = `${api.API_PREFIX}/cluster`;
   513      const clusterID = "12345abcde";
   514  
   515      afterEach(fetchMock.restore);
   516  
   517      it("correctly requests cluster info", function () {
   518        this.timeout(1000);
   519        fetchMock.mock({
   520          matcher: clusterUrl,
   521          method: "GET",
   522          response: (_url: string, requestObj: RequestInit) => {
   523            assert.isUndefined(requestObj.body);
   524            const encodedResponse = protos.cockroach.server.serverpb.ClusterResponse.encode({ cluster_id: clusterID }).finish();
   525            return {
   526              body: api.toArrayBuffer(encodedResponse),
   527            };
   528          },
   529        });
   530  
   531        return api.getCluster(new protos.cockroach.server.serverpb.ClusterRequest()).then((result) => {
   532          assert.lengthOf(fetchMock.calls(clusterUrl), 1);
   533          assert.deepEqual(result.cluster_id, clusterID);
   534        });
   535      });
   536  
   537      it("correctly handles an error", function (done) {
   538        this.timeout(1000);
   539  
   540        // Mock out the fetch query, but return an error
   541        fetchMock.mock({
   542          matcher: clusterUrl,
   543          method: "GET",
   544          response: (_url: string, requestObj: RequestInit) => {
   545            assert.isUndefined(requestObj.body);
   546            return { throws: new Error() };
   547          },
   548        });
   549  
   550        api.getCluster(new protos.cockroach.server.serverpb.ClusterRequest()).then((_result) => {
   551          done(new Error("Request unexpectedly succeeded."));
   552        }).catch(function (e) {
   553          assert(_.isError(e));
   554          done();
   555        });
   556      });
   557  
   558      it("correctly times out", function (done) {
   559        this.timeout(1000);
   560        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   561        fetchMock.mock({
   562          matcher: clusterUrl,
   563          method: "GET",
   564          response: (_url: string, requestObj: RequestInit) => {
   565            assert.isUndefined(requestObj.body);
   566            return new Promise<any>(() => { });
   567          },
   568        });
   569  
   570        api.getCluster(new protos.cockroach.server.serverpb.ClusterRequest(), moment.duration(0)).then((_result) => {
   571          done(new Error("Request unexpectedly succeeded."));
   572        }).catch(function (e) {
   573          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   574          done();
   575        });
   576      });
   577    });
   578  
   579    describe("metrics metadata request", function() {
   580      const metricMetadataUrl = `${api.API_PREFIX}/metricmetadata`;
   581      afterEach(fetchMock.restore);
   582  
   583      it("returns list of metadata metrics", () => {
   584        this.timeout(1000);
   585        const metadata = {};
   586        fetchMock.mock({
   587           matcher: metricMetadataUrl,
   588           method: "GET",
   589           response: (_url: string, requestObj: RequestInit) => {
   590             assert.isUndefined(requestObj.body);
   591             const encodedResponse = protos.cockroach.server.serverpb.MetricMetadataResponse.encode({ metadata }).finish();
   592             return {
   593               body: api.toArrayBuffer(encodedResponse),
   594             };
   595           },
   596         });
   597  
   598        return api.getAllMetricMetadata(new protos.cockroach.server.serverpb.MetricMetadataRequest()).then((result) => {
   599          assert.lengthOf(fetchMock.calls(metricMetadataUrl), 1);
   600          assert.deepEqual(result.metadata, metadata);
   601        });
   602      });
   603  
   604      it("correctly handles an error", function (done) {
   605        this.timeout(1000);
   606  
   607        // Mock out the fetch query, but return an error
   608        fetchMock.mock({
   609          matcher: metricMetadataUrl,
   610          method: "GET",
   611          response: (_url: string, requestObj: RequestInit) => {
   612            assert.isUndefined(requestObj.body);
   613            return { throws: new Error() };
   614          },
   615        });
   616  
   617        api.getAllMetricMetadata(new protos.cockroach.server.serverpb.MetricMetadataRequest()).then((_result) => {
   618          done(new Error("Request unexpectedly succeeded."));
   619        }).catch(function (e) {
   620          assert(_.isError(e));
   621          done();
   622        });
   623      });
   624  
   625      it("correctly times out", function (done) {
   626        this.timeout(1000);
   627        // Mock out the fetch query, but return a promise that's never resolved to test the timeout
   628        fetchMock.mock({
   629          matcher: metricMetadataUrl,
   630          method: "GET",
   631          response: (_url: string, requestObj: RequestInit) => {
   632            assert.isUndefined(requestObj.body);
   633            return new Promise<any>(() => { });
   634          },
   635        });
   636  
   637        api.getAllMetricMetadata(new protos.cockroach.server.serverpb.MetricMetadataRequest(), moment.duration(0)).then((_result) => {
   638          done(new Error("Request unexpectedly succeeded."));
   639        }).catch(function (e) {
   640          assert(_.startsWith(e.message, "Promise timed out"), "Error is a timeout error.");
   641          done();
   642        });
   643      });
   644    });
   645  });