github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/uiData.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 { Action } from "redux";
    14  import * as protobuf from "protobufjs/minimal";
    15  import fetchMock from "src/util/fetch-mock";
    16  
    17  import * as protos from "src/js/protos";
    18  import * as api from "src/util/api";
    19  import * as uidata from "./uiData";
    20  
    21  describe("UIData reducer", function() {
    22    describe("actions", function() {
    23      it("setUIDataKey() creates the correct action type.", function() {
    24        assert.equal(uidata.setUIDataKey("string", null).type, uidata.SET);
    25      });
    26  
    27      it("beginSaveUIData() creates the correct action type.", function() {
    28        assert.equal(uidata.beginSaveUIData([]).type, uidata.SAVE);
    29      });
    30  
    31      it("saveErrorUIData() creates the correct action type.", function() {
    32        assert.equal(uidata.saveErrorUIData(null, null).type, uidata.SAVE_ERROR);
    33      });
    34  
    35      it("beginLoadUIData() creates the correct action type.", function() {
    36        assert.equal(uidata.beginLoadUIData([]).type, uidata.LOAD);
    37      });
    38  
    39      it("loadErrorUIData() creates the correct action type.", function() {
    40        assert.equal(uidata.loadErrorUIData(null, null).type, uidata.LOAD_ERROR);
    41      });
    42    });
    43  
    44    describe("helper functions", function () {
    45      let state: any;
    46  
    47      beforeEach(function () {
    48        state = { uiData: uidata.uiDataReducer(undefined, { type: "unknown" }) };
    49      });
    50  
    51      const dispatch = (action: Action) => {
    52        state = { uiData: uidata.uiDataReducer(state.uiData, action) };
    53      };
    54  
    55      it("isValid", function () {
    56        const key1 = "key1";
    57        const key2 = "key2";
    58  
    59        assert.isFalse(uidata.isValid(state, key1));
    60        assert.isFalse(uidata.isValid(state, key2));
    61  
    62        dispatch(uidata.setUIDataKey(key1, null));
    63  
    64        assert(uidata.isValid(state, key1));
    65        assert.isFalse(uidata.isValid(state, key2));
    66  
    67        dispatch(uidata.setUIDataKey(key2, null));
    68  
    69        assert(uidata.isValid(state, key1));
    70        assert(uidata.isValid(state, key2));
    71      });
    72  
    73      it("getData", function() {
    74        const key1 = "key1";
    75        const key2 = "key2";
    76        const value1 = "value1";
    77        const value2 = "value2";
    78  
    79        assert.isUndefined(uidata.getData(state, key1));
    80        assert.isUndefined(uidata.getData(state, key2));
    81  
    82        dispatch(uidata.setUIDataKey(key1, value1));
    83  
    84        assert.equal(uidata.getData(state, key1), value1);
    85        assert.isUndefined(uidata.getData(state, key2));
    86  
    87        dispatch(uidata.setUIDataKey(key2, value2));
    88  
    89        assert.equal(uidata.getData(state, key1), value1);
    90        assert.equal(uidata.getData(state, key2), value2);
    91      });
    92  
    93      it("isSaving and isInFlight", function () {
    94        const key1 = "key1";
    95        const key2 = "key2";
    96        const saving = (k: string) => uidata.isSaving(state, k);
    97        const inFlight = (k: string) => uidata.isInFlight(state, k);
    98  
    99        assert.isFalse(saving(key1));
   100        assert.isFalse(inFlight(key1));
   101        assert.isFalse(saving(key2));
   102        assert.isFalse(inFlight(key2));
   103  
   104        dispatch(uidata.beginSaveUIData([key1]));
   105  
   106        assert(saving(key1));
   107        assert(inFlight(key1));
   108        assert.isFalse(saving(key2));
   109        assert.isFalse(inFlight(key2));
   110  
   111        dispatch(uidata.beginSaveUIData([key1, key2]));
   112  
   113        assert(saving(key1));
   114        assert(inFlight(key1));
   115        assert(saving(key2));
   116        assert(inFlight(key2));
   117  
   118        dispatch(uidata.beginLoadUIData([key1, key2]));
   119  
   120        assert.isFalse(saving(key1));
   121        assert(inFlight(key1));
   122        assert.isFalse(saving(key2));
   123        assert(inFlight(key2));
   124  
   125        dispatch(uidata.setUIDataKey(key2, null));
   126  
   127        assert.isFalse(saving(key1));
   128        assert(inFlight(key1));
   129        assert.isFalse(saving(key2));
   130        assert.isFalse(inFlight(key2));
   131      });
   132  
   133      it("getSaveError and getLoadError", function () {
   134        const key1 = "key1";
   135        const key2 = "key2";
   136        const saveError = (k: string) => uidata.getSaveError(state, k);
   137        const loadError = (k: string) => uidata.getLoadError(state, k);
   138  
   139        assert.isNull(saveError(key1));
   140        assert.isNull(saveError(key2));
   141        assert.isNull(loadError(key1));
   142        assert.isNull(loadError(key2));
   143  
   144        let e = new Error();
   145        dispatch(uidata.saveErrorUIData(key1, e));
   146  
   147        assert.equal(saveError(key1), e);
   148        assert.isNull(saveError(key2));
   149        assert.isNull(loadError(key1));
   150        assert.isNull(loadError(key2));
   151  
   152        e = new Error();
   153        dispatch(uidata.loadErrorUIData(key1, e));
   154  
   155        assert.isNull(saveError(key1));
   156        assert.isNull(saveError(key2));
   157        assert.equal(loadError(key1), e);
   158        assert.isNull(loadError(key2));
   159  
   160        dispatch(uidata.beginLoadUIData([key1]));
   161  
   162        assert.isNull(saveError(key1));
   163        assert.isNull(saveError(key2));
   164        assert.equal(loadError(key1), e);
   165        assert.isNull(loadError(key2));
   166  
   167        let e2 = new Error();
   168        dispatch(uidata.saveErrorUIData(key2, e2));
   169  
   170        assert.isNull(saveError(key1));
   171        assert.equal(saveError(key2), e2);
   172        assert.equal(loadError(key1), e);
   173        assert.isNull(loadError(key2));
   174  
   175        e2 = new Error();
   176        dispatch(uidata.loadErrorUIData(key2, e2));
   177  
   178        assert.isNull(saveError(key1));
   179        assert.isNull(saveError(key2));
   180        assert.equal(loadError(key1), e);
   181        assert.equal(loadError(key2), e2);
   182  
   183        dispatch(uidata.beginLoadUIData([key2]));
   184  
   185        assert.isNull(saveError(key1));
   186        assert.isNull(saveError(key2));
   187        assert.equal(loadError(key1), e);
   188        assert.equal(loadError(key2), e2);
   189  
   190        dispatch(uidata.setUIDataKey(key1, null));
   191  
   192        assert.isNull(saveError(key1));
   193        assert.isNull(saveError(key2));
   194        assert.isNull(loadError(key1));
   195        assert.equal(loadError(key2), e2);
   196  
   197        dispatch(uidata.setUIDataKey(key2, null));
   198  
   199        assert.isNull(saveError(key1));
   200        assert.isNull(saveError(key2));
   201        assert.isNull(loadError(key1));
   202        assert.isNull(loadError(key2));
   203      });
   204    });
   205  
   206    describe("reducer", function() {
   207      let state: uidata.UIDataState;
   208  
   209      beforeEach(function () {
   210        state = uidata.uiDataReducer(undefined, { type: "unknown" });
   211      });
   212  
   213      const dispatch = (action: Action) => {
   214        state = uidata.uiDataReducer(state, action);
   215      };
   216  
   217      it("should have the correct default value.", function() {
   218        const expected = {};
   219        assert.deepEqual(state, expected);
   220      });
   221  
   222      it("should correctly dispatch setUIDataKey.", function() {
   223        const objKey = "obj";
   224        const boolKey = "bool";
   225        const numKey = "num";
   226        const obj = { value: 1 };
   227        const bool = true;
   228        const num = 240;
   229  
   230        assert.isUndefined(state[objKey]);
   231        assert.isUndefined(state[boolKey]);
   232        assert.isUndefined(state[numKey]);
   233  
   234        // Validate setting a variety of object types.
   235        dispatch(uidata.setUIDataKey(objKey, obj));
   236        dispatch(uidata.setUIDataKey(boolKey, bool));
   237        dispatch(uidata.setUIDataKey(numKey, num));
   238  
   239        assert.lengthOf(_.keys(state), 3);
   240        assert.equal(state[objKey].data, obj);
   241        assert.equal(state[objKey].status, uidata.UIDataStatus.VALID);
   242        assert.equal(state[boolKey].data, bool);
   243        assert.equal(state[boolKey].status, uidata.UIDataStatus.VALID);
   244        assert.equal(state[numKey].data, num);
   245        assert.equal(state[numKey].status, uidata.UIDataStatus.VALID);
   246  
   247        // validate overwrite.
   248        const obj2 = { value: 2 };
   249        dispatch(uidata.setUIDataKey(objKey, obj2));
   250        assert.lengthOf(_.keys(state), 3);
   251        assert.equal(state[objKey].data, obj2);
   252        assert.equal(state[objKey].status, uidata.UIDataStatus.VALID);
   253      });
   254  
   255      it("should correctly dispatch loadErrorUIData.", function () {
   256        const key1 = "key1";
   257        const err = new Error("an error.");
   258        assert.isUndefined(state[key1]);
   259        dispatch(uidata.loadErrorUIData(key1, err));
   260        assert.equal(state[key1].status, uidata.UIDataStatus.LOAD_ERROR);
   261        assert.equal(state[key1].error, err);
   262  
   263        dispatch(uidata.setUIDataKey(key1, 4));
   264        assert.equal(state[key1].status, uidata.UIDataStatus.VALID);
   265        assert.isNull(state[key1].error);
   266      });
   267  
   268      it("should correctly dispatch saveErrorUIData.", function () {
   269        const key1 = "key1";
   270        const err = new Error("an error.");
   271        assert.isUndefined(state[key1]);
   272        dispatch(uidata.saveErrorUIData(key1, err));
   273        assert.equal(state[key1].status, uidata.UIDataStatus.SAVE_ERROR);
   274        assert.equal(state[key1].error, err);
   275  
   276        dispatch(uidata.setUIDataKey(key1, 4));
   277        assert.equal(state[key1].status, uidata.UIDataStatus.VALID);
   278        assert.isNull(state[key1].error);
   279      });
   280  
   281      it("should correctly dispatch beginSaveUIData", function () {
   282        const key1 = "key1";
   283        const key2 = "key2";
   284        const keys = [key1, key2];
   285        dispatch(uidata.beginSaveUIData(keys));
   286        assert.lengthOf(_.keys(state), 2);
   287        assert.equal(state[key1].status, uidata.UIDataStatus.SAVING);
   288        assert.equal(state[key2].status, uidata.UIDataStatus.SAVING);
   289        dispatch(uidata.setUIDataKey(key1, "value1"));
   290        dispatch(uidata.setUIDataKey(key2, "value2"));
   291        assert.equal(state[key1].status, uidata.UIDataStatus.VALID);
   292        assert.equal(state[key2].status, uidata.UIDataStatus.VALID);
   293      });
   294  
   295      it("should correctly dispatch beginLoadUIData", function () {
   296        const key1 = "key1";
   297        const key2 = "key2";
   298        const keys = [key1, key2];
   299        dispatch(uidata.beginLoadUIData(keys));
   300        assert.lengthOf(_.keys(state), 2);
   301        assert.equal(state[key1].status, uidata.UIDataStatus.LOADING);
   302        assert.equal(state[key2].status, uidata.UIDataStatus.LOADING);
   303        dispatch(uidata.setUIDataKey(key1, "value1"));
   304        dispatch(uidata.setUIDataKey(key2, "value2"));
   305        assert.equal(state[key1].status, uidata.UIDataStatus.VALID);
   306        assert.equal(state[key2].status, uidata.UIDataStatus.VALID);
   307      });
   308    });
   309  
   310    describe("asynchronous actions", function() {
   311      let state: uidata.UIDataState;
   312  
   313      const dispatch = (action: Action) => {
   314        state = uidata.uiDataReducer(state, action);
   315      };
   316  
   317      const uiKey1 = "a_key";
   318      const uiObj1 = {
   319        setting1: "value",
   320        setting2: true,
   321      };
   322  
   323      const uiKey2 = "another_key";
   324      const uiObj2 = 1234;
   325  
   326      const saveUIData = function(...values: uidata.KeyValue[]): Promise<void> {
   327        return uidata.saveUIData.apply(this, values)(dispatch, () => { return { uiData: state }; });
   328      };
   329  
   330      const loadUIData = function(...keys: string[]): Promise<void> {
   331        return uidata.loadUIData.apply(this, keys)(dispatch, () => { return { uiData: state }; });
   332      };
   333  
   334      beforeEach(function () {
   335        state = uidata.uiDataReducer(undefined, { type: "unknown" });
   336      });
   337  
   338      afterEach(fetchMock.restore);
   339  
   340      it("correctly saves UIData", function() {
   341        fetchMock.mock({
   342          matcher: `${api.API_PREFIX}/uidata`,
   343          method: "POST",
   344          response: (_url: string, requestObj: RequestInit) => {
   345            assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVING);
   346            assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVING);
   347  
   348            const kvs = protos.cockroach.server.serverpb.SetUIDataRequest.decode(new Uint8Array(requestObj.body as ArrayBuffer)).key_values;
   349  
   350            assert.lengthOf(_.keys(kvs), 2);
   351  
   352            const deserialize = function(buffer: Uint8Array): Object {
   353              return JSON.parse(protobuf.util.utf8.read(buffer, 0, buffer.byteLength));
   354            };
   355  
   356            assert.deepEqual(deserialize(kvs[uiKey1]), uiObj1);
   357            assert.deepEqual(deserialize(kvs[uiKey2]), uiObj2);
   358  
   359            const encodedResponse = protos.cockroach.server.serverpb.SetUIDataResponse.encode({}).finish();
   360            return {
   361              body: api.toArrayBuffer(encodedResponse),
   362            };
   363          },
   364        });
   365  
   366        const p = saveUIData(
   367          {key: uiKey1, value: uiObj1},
   368          {key: uiKey2, value: uiObj2},
   369        );
   370  
   371        // Second save should be ignored.
   372        const p2 = saveUIData(
   373          {key: uiKey1, value: uiObj1},
   374          {key: uiKey2, value: uiObj2},
   375        );
   376  
   377        return Promise.all([p, p2]).then(() => {
   378          assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/uidata`), 1);
   379          assert.lengthOf(_.keys(state), 2);
   380          assert.equal(state[uiKey1].data, uiObj1);
   381          assert.equal(state[uiKey2].data, uiObj2);
   382          assert.isNull(state[uiKey1].error);
   383          assert.isNull(state[uiKey2].error);
   384          assert.equal(state[uiKey1].status, uidata.UIDataStatus.VALID);
   385          assert.equal(state[uiKey2].status, uidata.UIDataStatus.VALID);
   386        });
   387      });
   388  
   389      it("correctly reacts to error during save", function (done) {
   390        this.timeout(2000);
   391        fetchMock.mock({
   392          matcher: `${api.API_PREFIX}/uidata`,
   393          method: "POST",
   394          response: () => {
   395            return { throws: new Error(), status: 500};
   396          },
   397        });
   398  
   399        const p = saveUIData(
   400          {key: uiKey1, value: uiObj1},
   401          {key: uiKey2, value: uiObj2},
   402        );
   403  
   404        p.then(() => {
   405          assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/uidata`), 1);
   406          assert.lengthOf(_.keys(state), 2);
   407          assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVING);
   408          assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVING);
   409          assert.isUndefined(state[uiKey1].data);
   410          assert.isUndefined(state[uiKey2].data);
   411          assert.notProperty(state[uiKey1], "data");
   412          assert.notProperty(state[uiKey2], "data");
   413          assert.isUndefined(state[uiKey1].error);
   414          assert.isUndefined(state[uiKey2].error);
   415          setTimeout(
   416            () => {
   417              assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVE_ERROR);
   418              assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVE_ERROR);
   419              assert.instanceOf(state[uiKey1].error, Error);
   420              assert.instanceOf(state[uiKey2].error, Error);
   421              done();
   422            },
   423            1000,
   424          );
   425        });
   426      });
   427  
   428      it("correctly loads UIData", function() {
   429        const expectedURL = `${api.API_PREFIX}/uidata?keys=${uiKey1}&keys=${uiKey2}`;
   430  
   431        fetchMock.mock({
   432          matcher: expectedURL,
   433          method: "GET",
   434          response: () => {
   435            // FetchMock URL must match the above string exactly, requesting both
   436            // keys.
   437            assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOADING);
   438            assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOADING);
   439  
   440            const response: protos.cockroach.server.serverpb.IGetUIDataResponse = {
   441              key_values: {},
   442            };
   443            const setValue = function(key: string, obj: Object) {
   444              const stringifiedValue = JSON.stringify(obj);
   445              const buffer = new Uint8Array(protobuf.util.utf8.length(stringifiedValue));
   446              protobuf.util.utf8.write(stringifiedValue, buffer, 0);
   447              response.key_values[key] = { value: buffer };
   448            };
   449            setValue(uiKey1, uiObj1);
   450            setValue(uiKey2, uiObj2);
   451  
   452            const encodedResponse = protos.cockroach.server.serverpb.GetUIDataResponse.encode(response).finish();
   453            return {
   454              body: api.toArrayBuffer(encodedResponse),
   455            };
   456          },
   457        });
   458  
   459        const p = loadUIData(uiKey1, uiKey2);
   460        const p2 = loadUIData(uiKey1, uiKey2); // Second load should be ignored.
   461  
   462        return Promise.all([p, p2]).then(() => {
   463          assert.lengthOf(fetchMock.calls(expectedURL), 1);
   464          assert.lengthOf(_.keys(state), 2);
   465          assert.deepEqual(state[uiKey1].data, uiObj1);
   466          assert.deepEqual(state[uiKey2].data, uiObj2);
   467          assert.isNull(state[uiKey1].error);
   468          assert.isNull(state[uiKey2].error);
   469          assert.equal(state[uiKey1].status, uidata.UIDataStatus.VALID);
   470          assert.equal(state[uiKey2].status, uidata.UIDataStatus.VALID);
   471        });
   472      });
   473  
   474      it("correctly reacts to error during load", function (done) {
   475        this.timeout(2000);
   476  
   477        const uidataPrefixMatcher = `begin:${api.API_PREFIX}/uidata`;
   478  
   479        fetchMock.mock({
   480          matcher: uidataPrefixMatcher,
   481          response: () => {
   482            return { throws: new Error() };
   483          },
   484        });
   485  
   486        const p = loadUIData(uiKey1, uiKey2);
   487  
   488        p.then(() => {
   489          assert.lengthOf(fetchMock.calls(uidataPrefixMatcher), 1);
   490          assert.lengthOf(_.keys(state), 2);
   491          assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOADING);
   492          assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOADING);
   493          assert.isUndefined(state[uiKey1].data);
   494          assert.isUndefined(state[uiKey2].data);
   495          assert.notProperty(state[uiKey1], "data");
   496          assert.notProperty(state[uiKey2], "data");
   497          assert.isUndefined(state[uiKey1].error);
   498          assert.isUndefined(state[uiKey2].error);
   499          setTimeout(
   500            () => {
   501              assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOAD_ERROR);
   502              assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOAD_ERROR);
   503              assert.instanceOf(state[uiKey1].error, Error);
   504              assert.instanceOf(state[uiKey2].error, Error);
   505              done();
   506            },
   507            1000,
   508          );
   509        });
   510      });
   511  
   512      it("handles missing keys", function () {
   513        const missingKey = "missingKey";
   514  
   515        const expectedURL = `${api.API_PREFIX}/uidata?keys=${missingKey}`;
   516  
   517        fetchMock.mock({
   518          matcher: expectedURL,
   519          response: () => {
   520            assert.equal(state[missingKey].status, uidata.UIDataStatus.LOADING);
   521  
   522            const encodedResponse = protos.cockroach.server.serverpb.GetUIDataResponse.encode({}).finish();
   523            return {
   524              body: api.toArrayBuffer(encodedResponse),
   525            };
   526          },
   527        });
   528  
   529        const p = loadUIData(missingKey);
   530  
   531        return p.then(() => {
   532          assert.lengthOf(fetchMock.calls(expectedURL), 1);
   533          assert.lengthOf(_.keys(state), 1);
   534          assert.equal(state[missingKey].data, undefined);
   535          assert.property(state[missingKey], "data");
   536          assert.equal(state[missingKey].status, uidata.UIDataStatus.VALID);
   537          assert.isNull(state[missingKey].error);
   538        });
   539      });
   540    });
   541  });