github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/alerts.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 { Store } from "redux";
    13  import moment from "moment";
    14  import sinon from "sinon";
    15  import { createHashHistory } from "history";
    16  
    17  import * as protos from "src/js/protos";
    18  import { API_PREFIX } from "src/util/api";
    19  import fetchMock from "src/util/fetch-mock";
    20  
    21  import { AdminUIState, createAdminUIStore } from "./state";
    22  import {
    23    AlertLevel,
    24    alertDataSync,
    25    staggeredVersionWarningSelector,
    26    staggeredVersionDismissedSetting,
    27    newVersionNotificationSelector,
    28    newVersionDismissedLocalSetting,
    29    disconnectedAlertSelector,
    30    disconnectedDismissedLocalSetting,
    31    emailSubscriptionAlertLocalSetting,
    32    emailSubscriptionAlertSelector,
    33  } from "./alerts";
    34  import { versionsSelector } from "src/redux/nodes";
    35  import {
    36    VERSION_DISMISSED_KEY, INSTRUCTIONS_BOX_COLLAPSED_KEY,
    37    setUIDataKey, isInFlight,
    38  } from "./uiData";
    39  import {
    40    livenessReducerObj, versionReducerObj, nodesReducerObj, clusterReducerObj, healthReducerObj,
    41  } from "./apiReducers";
    42  
    43  const sandbox = sinon.createSandbox();
    44  
    45  describe("alerts", function() {
    46    let store: Store<AdminUIState>;
    47    let dispatch: typeof store.dispatch;
    48    let state: typeof store.getState;
    49  
    50    beforeEach(function () {
    51      store = createAdminUIStore(createHashHistory());
    52      dispatch = store.dispatch;
    53      state = store.getState;
    54      // localSettings persist values in sessionStorage and
    55      // this stub disables caching values between tests.
    56      sandbox.stub(sessionStorage, "getItem").returns(null);
    57    });
    58  
    59    afterEach(function() {
    60      sandbox.restore();
    61      fetchMock.restore();
    62    });
    63  
    64    describe("selectors", function() {
    65      describe("versions", function() {
    66        it("tolerates missing liveness data", function () {
    67          dispatch(nodesReducerObj.receiveData([
    68            {
    69              build_info: {
    70                tag: "0.1",
    71              },
    72            },
    73            {
    74              build_info: {
    75                tag: "0.2",
    76              },
    77            },
    78          ]));
    79          const versions = versionsSelector(state());
    80          assert.deepEqual(versions, ["0.1", "0.2"]);
    81        });
    82  
    83        it("ignores decommissioned nodes", function () {
    84          dispatch(nodesReducerObj.receiveData([
    85            {
    86              build_info: {
    87                tag: "0.1",
    88              },
    89            },
    90            {
    91              desc: {
    92                node_id: 2,
    93              },
    94              build_info: {
    95                tag: "0.2",
    96              },
    97            },
    98          ]));
    99  
   100          dispatch(livenessReducerObj.receiveData(
   101            new protos.cockroach.server.serverpb.LivenessResponse({
   102              livenesses: [{
   103                node_id: 2,
   104                decommissioning: true,
   105              }],
   106            }),
   107          ));
   108  
   109          const versions = versionsSelector(state());
   110          assert.deepEqual(versions, ["0.1"]);
   111        });
   112      });
   113  
   114      describe("version mismatch warning", function () {
   115        it("requires versions to be loaded before displaying", function () {
   116          const alert = staggeredVersionWarningSelector(state());
   117          assert.isUndefined(alert);
   118        });
   119  
   120        it("does not display when versions match", function () {
   121          dispatch(nodesReducerObj.receiveData([
   122            {
   123              build_info: {
   124                tag: "0.1",
   125              },
   126            },
   127            {
   128              build_info: {
   129                tag: "0.1",
   130              },
   131            },
   132          ]));
   133          const alert = staggeredVersionWarningSelector(state());
   134          assert.isUndefined(alert);
   135        });
   136  
   137        it("displays when mismatch detected and not dismissed", function () {
   138          dispatch(nodesReducerObj.receiveData([
   139            {
   140              // `desc` intentionally omitted (must not affect outcome).
   141              build_info: {
   142                tag: "0.1",
   143              },
   144            },
   145            {
   146              desc: {
   147                node_id: 1,
   148              },
   149              build_info: {
   150                tag: "0.2",
   151              },
   152            },
   153          ]));
   154          const alert = staggeredVersionWarningSelector(state());
   155          assert.isObject(alert);
   156          assert.equal(alert.level, AlertLevel.WARNING);
   157          assert.equal(alert.title, "Staggered Version");
   158        });
   159  
   160        it("does not display if dismissed locally", function () {
   161          dispatch(nodesReducerObj.receiveData([
   162            {
   163              build_info: {
   164                tag: "0.1",
   165              },
   166            },
   167            {
   168              build_info: {
   169                tag: "0.2",
   170              },
   171            },
   172          ]));
   173          dispatch(staggeredVersionDismissedSetting.set(true));
   174          const alert = staggeredVersionWarningSelector(state());
   175          assert.isUndefined(alert);
   176        });
   177  
   178        it("dismisses by setting local dismissal", function () {
   179          dispatch(nodesReducerObj.receiveData([
   180            {
   181              build_info: {
   182                tag: "0.1",
   183              },
   184            },
   185            {
   186              build_info: {
   187                tag: "0.2",
   188              },
   189            },
   190          ]));
   191          const alert = staggeredVersionWarningSelector(state());
   192          dispatch(alert.dismiss);
   193          assert.isTrue(staggeredVersionDismissedSetting.selector(state()));
   194        });
   195      });
   196  
   197      describe("new version available notification", function () {
   198        it("displays nothing when versions have not yet been loaded", function () {
   199          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null));
   200          const alert = newVersionNotificationSelector(state());
   201          assert.isUndefined(alert);
   202        });
   203  
   204        it("displays nothing when persistent dismissal has not been checked", function () {
   205          dispatch(versionReducerObj.receiveData({
   206            details: [
   207              {
   208                version: "0.1",
   209                detail: "alpha",
   210              },
   211            ],
   212          }));
   213          const alert = newVersionNotificationSelector(state());
   214          assert.isUndefined(alert);
   215        });
   216  
   217        it("displays nothing when no new version is available", function () {
   218          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null));
   219          dispatch(versionReducerObj.receiveData({
   220            details: [],
   221          }));
   222          const alert = newVersionNotificationSelector(state());
   223          assert.isUndefined(alert);
   224        });
   225  
   226        it("displays when new version available and not dismissed", function () {
   227          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null));
   228          dispatch(versionReducerObj.receiveData({
   229            details: [
   230              {
   231                version: "0.1",
   232                detail: "alpha",
   233              },
   234            ],
   235          }));
   236          const alert = newVersionNotificationSelector(state());
   237          assert.isObject(alert);
   238          assert.equal(alert.level, AlertLevel.NOTIFICATION);
   239          assert.equal(alert.title, "New Version Available");
   240        });
   241  
   242        it("respects local dismissal setting", function () {
   243          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null));
   244          dispatch(versionReducerObj.receiveData({
   245            details: [
   246              {
   247                version: "0.1",
   248                detail: "alpha",
   249              },
   250            ],
   251          }));
   252          dispatch(newVersionDismissedLocalSetting.set(moment()));
   253          let alert = newVersionNotificationSelector(state());
   254          assert.isUndefined(alert);
   255  
   256          // Local dismissal only lasts one day.
   257          dispatch(newVersionDismissedLocalSetting.set(moment().subtract(2, "days")));
   258          alert = newVersionNotificationSelector(state());
   259          assert.isDefined(alert);
   260        });
   261  
   262        it("respects persistent dismissal setting", function () {
   263          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, moment().valueOf()));
   264          dispatch(versionReducerObj.receiveData({
   265            details: [
   266              {
   267                version: "0.1",
   268                detail: "alpha",
   269              },
   270            ],
   271          }));
   272          let alert = newVersionNotificationSelector(state());
   273          assert.isUndefined(alert);
   274  
   275          // Dismissal only lasts one day.
   276          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, moment().subtract(2, "days").valueOf()));
   277          alert = newVersionNotificationSelector(state());
   278          assert.isDefined(alert);
   279        });
   280  
   281        it("dismisses by setting local and persistent dismissal", function (done) {
   282          fetchMock.mock({
   283            matcher: `${API_PREFIX}/uidata`,
   284            method: "POST",
   285            response: (_url: string) => {
   286              const encodedResponse = protos.cockroach.server.serverpb.SetUIDataResponse.encode({}).finish();
   287              return {
   288                body: encodedResponse,
   289              };
   290            },
   291          });
   292  
   293          dispatch(setUIDataKey(VERSION_DISMISSED_KEY, null));
   294          dispatch(versionReducerObj.receiveData({
   295            details: [
   296              {
   297                version: "0.1",
   298                detail: "alpha",
   299              },
   300            ],
   301          }));
   302          const alert = newVersionNotificationSelector(state());
   303          const beforeDismiss = moment();
   304  
   305          dispatch(alert.dismiss).then(() => {
   306            assert.isTrue(newVersionDismissedLocalSetting.selector(state()).isSameOrAfter(beforeDismiss));
   307            assert.isNotNull(state().uiData[VERSION_DISMISSED_KEY]);
   308            assert.isNotNull(state().uiData[VERSION_DISMISSED_KEY].data);
   309            const dismissedMoment = moment(state().uiData[VERSION_DISMISSED_KEY].data as number);
   310            assert.isTrue(dismissedMoment.isSameOrAfter(beforeDismiss));
   311            done();
   312          });
   313        });
   314      });
   315  
   316      describe("disconnected alert", function () {
   317        it("requires health to be available before displaying", function () {
   318          const alert = disconnectedAlertSelector(state());
   319          assert.isUndefined(alert);
   320        });
   321  
   322        it("does not display when cluster is healthy", function () {
   323          dispatch(healthReducerObj.receiveData(
   324            new protos.cockroach.server.serverpb.ClusterResponse({})),
   325          );
   326          const alert = disconnectedAlertSelector(state());
   327          assert.isUndefined(alert);
   328        });
   329  
   330        it("displays when cluster health endpoint returns an error", function () {
   331          dispatch(healthReducerObj.errorData(new Error("error")));
   332          const alert = disconnectedAlertSelector(state());
   333          assert.isObject(alert);
   334          assert.equal(alert.level, AlertLevel.CRITICAL);
   335          assert.equal(alert.title, "We're currently having some trouble fetching updated data. If this persists, it might be a good idea to check your network connection to the CockroachDB cluster.");
   336        });
   337  
   338        it("does not display if dismissed locally", function () {
   339          dispatch(healthReducerObj.errorData(new Error("error")));
   340          dispatch(disconnectedDismissedLocalSetting.set(moment()));
   341          const alert = disconnectedAlertSelector(state());
   342          assert.isUndefined(alert);
   343        });
   344  
   345        it("dismisses by setting local dismissal", function (done) {
   346          dispatch(healthReducerObj.errorData(new Error("error")));
   347          const alert = disconnectedAlertSelector(state());
   348          const beforeDismiss = moment();
   349  
   350          dispatch(alert.dismiss).then(() => {
   351            assert.isTrue(
   352              disconnectedDismissedLocalSetting.selector(state()).isSameOrAfter(beforeDismiss),
   353            );
   354            done();
   355          });
   356        });
   357      });
   358  
   359      describe("email signup for release notes alert", () => {
   360        it("initialized with default 'false' (hidden) state", () => {
   361          const settingState = emailSubscriptionAlertLocalSetting.selector(state());
   362          assert.isFalse(settingState);
   363        });
   364  
   365        it("dismissed by alert#dismiss", async () => {
   366          // set alert to open state
   367          dispatch(emailSubscriptionAlertLocalSetting.set(true));
   368          let openState = emailSubscriptionAlertLocalSetting.selector(state());
   369          assert.isTrue(openState);
   370  
   371          // dismiss alert
   372          const alert = emailSubscriptionAlertSelector(state());
   373          await alert.dismiss(dispatch, state);
   374          openState = emailSubscriptionAlertLocalSetting.selector(state());
   375          assert.isFalse(openState);
   376        });
   377      });
   378    });
   379  
   380    describe("data sync listener", function() {
   381      let sync: () => void;
   382      beforeEach(function() {
   383        // We don't care about the responses, we only care that the sync listener
   384        // is making requests, which can be verified using "inFlight" settings.
   385        fetchMock.mock({
   386          matcher: "*",
   387          method: "GET",
   388          response: () => 500,
   389        });
   390  
   391        sync = alertDataSync(store);
   392      });
   393  
   394      it("dispatches requests for expected data on empty store", function() {
   395        sync();
   396        assert.isTrue(isInFlight(state(), VERSION_DISMISSED_KEY));
   397        assert.isTrue(state().cachedData.cluster.inFlight);
   398        assert.isTrue(state().cachedData.nodes.inFlight);
   399        assert.isFalse(state().cachedData.version.inFlight);
   400        assert.isTrue(state().cachedData.health.inFlight);
   401      });
   402  
   403      it("dispatches request for version data when cluster ID and nodes are available", function() {
   404        dispatch(nodesReducerObj.receiveData([
   405          {
   406            build_info: {
   407              tag: "0.1",
   408            },
   409          },
   410        ]));
   411        dispatch(clusterReducerObj.receiveData(new protos.cockroach.server.serverpb.ClusterResponse({
   412          cluster_id: "my-cluster",
   413        })));
   414  
   415        sync();
   416        assert.isTrue(state().cachedData.version.inFlight);
   417      });
   418  
   419      it("does not request version data when version is staggered", function() {
   420        dispatch(nodesReducerObj.receiveData([
   421          {
   422            build_info: {
   423              tag: "0.1",
   424            },
   425          },
   426          {
   427            build_info: {
   428              tag: "0.2",
   429            },
   430          },
   431        ]));
   432        dispatch(clusterReducerObj.receiveData(new protos.cockroach.server.serverpb.ClusterResponse({
   433          cluster_id: "my-cluster",
   434        })));
   435  
   436        sync();
   437        assert.isFalse(state().cachedData.version.inFlight);
   438      });
   439  
   440      it("refreshes health function whenever the last health response is no longer valid.", function() {
   441        dispatch(healthReducerObj.receiveData(
   442          new protos.cockroach.server.serverpb.ClusterResponse({})),
   443        );
   444        dispatch(healthReducerObj.invalidateData());
   445        sync();
   446        assert.isTrue(state().cachedData.health.inFlight);
   447      });
   448  
   449      it("does not do anything when all data is available.", function() {
   450        dispatch(nodesReducerObj.receiveData([
   451          {
   452            build_info: {
   453              tag: "0.1",
   454            },
   455          },
   456        ]));
   457        dispatch(clusterReducerObj.receiveData(new protos.cockroach.server.serverpb.ClusterResponse({
   458          cluster_id: "my-cluster",
   459        })));
   460        dispatch(setUIDataKey(VERSION_DISMISSED_KEY, "blank"));
   461        dispatch(setUIDataKey(INSTRUCTIONS_BOX_COLLAPSED_KEY, false));
   462        dispatch(versionReducerObj.receiveData({
   463          details: [],
   464        }));
   465        dispatch(healthReducerObj.receiveData(
   466          new protos.cockroach.server.serverpb.ClusterResponse({})),
   467        );
   468  
   469        const expectedState = state();
   470        sync();
   471        assert.deepEqual(state(), expectedState);
   472      });
   473    });
   474  });