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 });