Repository: FarmBot/Farmbot-Web-App (ts)

Issue:
reset password not working
# Expected Behavior
To be able to reset my password and log in

# Actual Behavior
I can't reset my password - i get an error "all device users must agree to tos"
but there's no where i can aggree to this TOS, and i can't log in to do so.....

![image](https://user-images.githubusercontent.com/12266/64650841-3a3ecd80-d474-11e9-8989-a77426377f4a.png)

# Steps to Reproduce
Reset password
follow link from email
type password twice
press "reset"

# Results of `git log -1`

Run `git log -1` and paste the results here.


Pull request description:
TOS bugfix
fixes #1424

Patch:
diff --git a/app/mutations/users/create.rb b/app/mutations/users/create.rb
index a49bbf605..d6766c840 100644
--- a/app/mutations/users/create.rb
+++ b/app/mutations/users/create.rb
@@ -4,7 +4,8 @@ module Users
     CANT_USE_SERVER = "You are not authorized to use this server. " \
                       "Please use an official email address."
     ALREADY_REGISTERED = "Already registered"
-    PW_MISMATCH = "Password and confirmation do not match."
+    PASSWORD_PROBLEMS = "Password less than 8 characters or does not match " \
+                        "password confirmation."
 
     required do
       string :name
@@ -23,8 +24,11 @@ module Users
       maybe_check_email_domain
       email.downcase!
       add_error :email, :*, ALREADY_REGISTERED if User.find_by(email: email)
-      if password != password_confirmation
-        add_error :password, :*, PW_MISMATCH
+      pw_length_ok = password.length > 7
+      pw_match = password == password_confirmation
+      pw_invalid = !(pw_match && pw_length_ok)
+      if pw_invalid
+        add_error :password, :*, PASSWORD_PROBLEMS
       end
     end
 
diff --git a/app/mutations/users/update.rb b/app/mutations/users/update.rb
index 9c5b918f2..08890fecc 100644
--- a/app/mutations/users/update.rb
+++ b/app/mutations/users/update.rb
@@ -1,6 +1,7 @@
 module Users
   class Update < Mutations::Command
-    PASSWORD_PROBLEMS = "Password and confirmation(s) must match."
+    PASSWORD_PROBLEMS = "Password and confirmation(s) do not match " +
+                        "or is less than 8 characters."
     EMAIL_IN_USE = "That email is already registered"
 
     required { model :user, class: User }
@@ -81,7 +82,7 @@ module Users
       valid_pw = user.valid_password?(password)
       has_new_pw = new_password && new_password_confirmation
       pws_match = new_password == new_password_confirmation
-      invalid = !(valid_pw && has_new_pw && pws_match)
+      invalid = !(valid_pw && has_new_pw && pws_match && new_password.length > 7)
       if invalid
         add_error :password, :*, PASSWORD_PROBLEMS
       else
diff --git a/frontend/connectivity/connect_device.ts b/frontend/connectivity/connect_device.ts
index b25370c62..fdf1e901a 100644
--- a/frontend/connectivity/connect_device.ts
+++ b/frontend/connectivity/connect_device.ts
@@ -26,12 +26,6 @@ import { t } from "../i18next_wrapper";
 import { now } from "../devices/connectivity/qos";
 
 export const TITLE = () => t("New message from bot");
-/** TODO: This ought to be stored in Redux. It is here because of historical
- * reasons. Feel free to factor out when time allows. -RC, 10 OCT 17 */
-export const HACKY_FLAGS = {
-  needVersionCheck: true,
-  alreadyToldUserAboutMalformedMsg: false
-};
 
 /** Action creator that is called when FarmBot OS emits a status update.
  * Coordinate updates, movement, etc.*/
@@ -116,12 +110,13 @@ export const changeLastClientConnected = (bot: Farmbot) => () => {
 };
 const setBothUp = () => bothUp();
 
-const legacyChecks = (getState: GetState) => {
+const legacyChecks = (dispatch: Function, getState: GetState) => {
   const { controller_version } = getState().bot.hardware.informational_settings;
-  if (HACKY_FLAGS.needVersionCheck && controller_version) {
+  const { needVersionCheck } = getState().bot;
+  if (needVersionCheck && controller_version) {
     const IS_OK = versionOK(controller_version);
     if (!IS_OK) { badVersion(); }
-    HACKY_FLAGS.needVersionCheck = false;
+    dispatch({ type: Actions.SET_NEEDS_VERSION_CHECK, payload: false });
   }
 };
 
@@ -130,7 +125,7 @@ export const onStatus =
     slowDown((msg: BotStateTree) => {
       setBothUp();
       dispatch(incomingStatus(msg));
-      legacyChecks(getState);
+      legacyChecks(dispatch, getState);
     });
 
 type Client = { connected?: boolean };
@@ -141,13 +136,14 @@ export const onSent = (client: Client) => () => {
   cb("user.mqtt", now());
 };
 
-export function onMalformed() {
+export const onMalformed = (dispatch: Function, getState: GetState) => () => {
+  const { alreadyToldUserAboutMalformedMsg } = getState().bot;
   bothUp();
-  if (!HACKY_FLAGS.alreadyToldUserAboutMalformedMsg) {
+  if (!alreadyToldUserAboutMalformedMsg) {
     warning(t(Content.MALFORMED_MESSAGE_REC_UPGRADE));
-    HACKY_FLAGS.alreadyToldUserAboutMalformedMsg = true;
+    dispatch({ type: Actions.SET_MALFORMED_NOTIFICATION_SENT, payload: true });
   }
-}
+};
 
 export const onOnline = () => {
   removeToast("offline");
@@ -184,7 +180,7 @@ export const attachEventListeners =
       bot.on(FbjsEventName.sent, onSent(bot.client));
       bot.on(FbjsEventName.logs, onLogs(dispatch, getState));
       bot.on(FbjsEventName.status, onStatus(dispatch, getState));
-      bot.on(FbjsEventName.malformed, onMalformed);
+      bot.on(FbjsEventName.malformed, onMalformed(dispatch, getState));
       bot.client.subscribe(FbjsEventName.publicBroadcast);
       bot.on(FbjsEventName.publicBroadcast, onPublicBroadcast);
       bot.client.on("message", autoSync(dispatch, getState));
diff --git a/frontend/constants.ts b/frontend/constants.ts
index ab188de61..c76c854a6 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -2170,6 +2170,8 @@ export enum Actions {
   FETCH_OS_RELEASE_NOTES_OK = "FETCH_OS_RELEASE_NOTES_OK",
   FETCH_OS_RELEASE_NOTES_ERROR = "FETCH_OS_RELEASE_NOTES_ERROR",
   STASH_STATUS = "STASH_STATUS",
+  SET_NEEDS_VERSION_CHECK = "SET_NEEDS_VERSION_CHECK",
+  SET_MALFORMED_NOTIFICATION_SENT = "SET_MALFORMED_NOTIFICATION_SENT",
 
   // Draggable
   PUT_DATA_XFER = "PUT_DATA_XFER",
diff --git a/frontend/devices/connectivity/fbos_metric_history_plot.tsx b/frontend/devices/connectivity/fbos_metric_history_plot.tsx
index 6f7e08e56..b307c7519 100644
--- a/frontend/devices/connectivity/fbos_metric_history_plot.tsx
+++ b/frontend/devices/connectivity/fbos_metric_history_plot.tsx
@@ -134,6 +134,37 @@ const PlotLines = (props: PlotLinesProps) => {
   </g>;
 };
 
+interface VersionChangeLinesProps {
+  telemetry: TaggedTelemetry[];
+}
+
+const VersionChangeLines = (props: VersionChangeLinesProps) => {
+  const changes: [number, string, string][] = [];
+  const mostRecent = last(sortBy(props.telemetry, "body.created_at"));
+  props.telemetry.map((d, i) => {
+    if (i == 0) { return; }
+    const previousVersion = props.telemetry[i - 1].body.fbos_version;
+    if (d.body.fbos_version && previousVersion &&
+      d.body.fbos_version != previousVersion) {
+      const x = clipX(d.body.created_at, mostRecent);
+      isNumber(x) && changes.push([x, previousVersion, d.body.fbos_version]);
+    }
+  });
+  return <g id="version-change-lines">
+    {changes.map(c =>
+      <g id={"" + c[0]}>
+        <line y1={0} y2={MAX_Y}
+          x1={plotXSeconds(c[0])}
+          x2={plotXSeconds(c[0])}
+          stroke={"gray"} strokeWidth={1} strokeDasharray={2} />
+        <text x={plotXSeconds(c[0]) - 3} y={5} color={"gray"}
+          textAnchor={"end"} style={{ textAnchor: "end" }}>v{c[1]}</text>
+        <text x={plotXSeconds(c[0]) + 3} y={5} color={"gray"}
+          textAnchor={"start"} style={{ textAnchor: "start" }}>v{c[2]}</text>
+      </g>)}
+  </g>;
+};
+
 export interface FbosMetricHistoryPlotProps {
   telemetry: TaggedTelemetry[];
   hoveredMetric: keyof Telemetry | undefined;
@@ -163,6 +194,7 @@ export const FbosMetricHistoryPlot = (props: FbosMetricHistoryPlotProps) => {
       <PlotLines telemetry={props.telemetry}
         hoveredMetric={props.hoveredMetric}
         onHover={props.onHover} />
+      <VersionChangeLines telemetry={props.telemetry} />
       {hoveredSeconds &&
         <line y1={0} y2={MAX_Y}
           x1={plotXSeconds(hoveredSeconds)}
diff --git a/frontend/devices/interfaces.ts b/frontend/devices/interfaces.ts
index 6bc73c71a..e4e4693e4 100644
--- a/frontend/devices/interfaces.ts
+++ b/frontend/devices/interfaces.ts
@@ -101,6 +101,8 @@ export interface BotState {
    * lets us know if it is safe to do data critical tasks with the bot */
   consistent: boolean;
   connectivity: ConnectionState;
+  needVersionCheck: boolean;
+  alreadyToldUserAboutMalformedMsg: boolean;
 }
 
 /** Status registers for the bot's status */
diff --git a/frontend/devices/reducer.ts b/frontend/devices/reducer.ts
index 303a10c03..0cf2bba1b 100644
--- a/frontend/devices/reducer.ts
+++ b/frontend/devices/reducer.ts
@@ -70,7 +70,9 @@ export const initialState = (): BotState => ({
       "user.api": undefined
     },
     pings: {}
-  }
+  },
+  needVersionCheck: true,
+  alreadyToldUserAboutMalformedMsg: false,
 });
 
 export const botReducer = generateReducer<BotState>(initialState())
@@ -116,6 +118,14 @@ export const botReducer = generateReducer<BotState>(initialState())
     stash(s);
     return s;
   })
+  .add<boolean>(Actions.SET_NEEDS_VERSION_CHECK, (s, { payload }) => {
+    s.needVersionCheck = payload;
+    return s;
+  })
+  .add<boolean>(Actions.SET_MALFORMED_NOTIFICATION_SENT, (s, { payload }) => {
+    s.alreadyToldUserAboutMalformedMsg = payload;
+    return s;
+  })
   .add<void>(Actions._RESOURCE_NO, (s) => {
     unstash(s);
     return s;
diff --git a/frontend/password_reset/password_reset.tsx b/frontend/password_reset/password_reset.tsx
index 0856fe2da..ebe62e2bd 100644
--- a/frontend/password_reset/password_reset.tsx
+++ b/frontend/password_reset/password_reset.tsx
@@ -7,6 +7,7 @@ import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../ui";
 import { Session } from "../session";
 import { t } from "../i18next_wrapper";
 import { ToastContainer } from "../toast/fb_toast";
+import { get } from "lodash";
 
 export interface State {
   password?: string;
@@ -26,6 +27,8 @@ export class PasswordReset extends React.Component<{}, State> {
     };
   }
 
+  update = () => setTimeout(() => this.forceUpdate(), 100);
+
   componentDidMount() {
     API.setBaseUrl(API.fetchBrowserLocation());
     this.setState({
@@ -51,7 +54,14 @@ export class PasswordReset extends React.Component<{}, State> {
     })
       .then(Session.clear)
       .catch((error: string) => {
-        log(prettyPrintApiErrors(error as {}));
+        switch (get(error, "response.status")) {
+          case 451: // TOS was updated; User must agree to terms.
+            window.location.assign("/tos_update");
+            break;
+          default:
+            log(prettyPrintApiErrors(error as {}));
+            this.update();
+        }
       });
   }
 
diff --git a/frontend/settings/account/change_password.tsx b/frontend/settings/account/change_password.tsx
index 863c633d8..588052470 100644
--- a/frontend/settings/account/change_password.tsx
+++ b/frontend/settings/account/change_password.tsx
@@ -51,11 +51,16 @@ export class ChangePassword extends React.Component<{}, ChangePWState> {
         success(t("Your password is changed."));
         this.clearForm();
       }, (e: AxiosErrorResponse) => {
-        error(e ? prettyPrintApiErrors(e) : t("Password change failed."));
+        error(prettyPrintApiErrors(e));
         this.clearForm();
       });
 
   save = () => {
+    if (this.state.form.new_password.length < 8) {
+      error(t("New password must be at least 8 characters."));
+      this.clearForm();
+      return;
+    }
     const numUniqueValues = uniq(Object.values(this.state.form)).length;
     switch (numUniqueValues) {
       case 1:
diff --git a/spec/controllers/api/users/users_controller_spec.rb b/spec/controllers/api/users/users_controller_spec.rb
index 65684ea82..07da4d40d 100644
--- a/spec/controllers/api/users/users_controller_spec.rb
+++ b/spec/controllers/api/users/users_controller_spec.rb
@@ -154,7 +154,7 @@ describe Api::UsersController do
                name: "Frank" }
     post :create, body: params.to_json
     expect(User.count > original_count).to be_falsy
-    expect(json[:password]).to include("do not match")
+    expect(json[:password]).to include("does not match")
     expect(response.status).to eq(422)
   end
 
diff --git a/spec/mutations/users/create_spec.rb b/spec/mutations/users/create_spec.rb
index fa58b9dd2..8fe82c539 100644
--- a/spec/mutations/users/create_spec.rb
+++ b/spec/mutations/users/create_spec.rb
@@ -62,4 +62,16 @@ describe Users::Create do
       errors = results.errors.message_list
       expect(errors).to include(Users::Create::ALREADY_REGISTERED)
     end
+
+    it "disallows short passwords and mismatches" do
+      email   = "#{SecureRandom.hex(8)}@qwerty.io"
+      results = Users::Create.run(email:                 email,
+                                  name:                  "Faker",
+                                  password:              "passwor",
+                                  password_confirmation: "x",
+                                  agree_to_terms:        false)
+      expect(results.success?).to be false
+      errors = results.errors.message_list
+      expect(errors).to include(Users::Create::PASSWORD_PROBLEMS)
+    end
 end


Test patch:
diff --git a/frontend/__test_support__/fake_state/bot.ts b/frontend/__test_support__/fake_state/bot.ts
index 69ca31724..1266dc9e8 100644
--- a/frontend/__test_support__/fake_state/bot.ts
+++ b/frontend/__test_support__/fake_state/bot.ts
@@ -54,5 +54,7 @@ export const bot: Everything["bot"] = {
       "user.api": undefined,
     },
     pings: {}
-  }
+  },
+  needVersionCheck: true,
+  alreadyToldUserAboutMalformedMsg: false,
 };
diff --git a/frontend/connectivity/__tests__/connect_device/index_test.ts b/frontend/connectivity/__tests__/connect_device/index_test.ts
index 5acf3a1a3..723b75f02 100644
--- a/frontend/connectivity/__tests__/connect_device/index_test.ts
+++ b/frontend/connectivity/__tests__/connect_device/index_test.ts
@@ -267,14 +267,22 @@ describe("onSent", () => {
 
 describe("onMalformed()", () => {
   it("handles malformed messages", () => {
-    onMalformed();
+    const dispatch = jest.fn();
+    const state = fakeState();
+    state.bot.alreadyToldUserAboutMalformedMsg = false;
+    onMalformed(dispatch, () => state)();
     expect(warning)
       .toHaveBeenCalledWith(Content.MALFORMED_MESSAGE_REC_UPGRADE);
+    expect(dispatch).toHaveBeenCalledWith({
+      type: Actions.SET_MALFORMED_NOTIFICATION_SENT, payload: true,
+    });
     jest.resetAllMocks();
-    onMalformed();
+    state.bot.alreadyToldUserAboutMalformedMsg = true;
+    onMalformed(dispatch, () => state)();
     expect(warning) // Only fire once.
       .not
       .toHaveBeenCalledWith(Content.MALFORMED_MESSAGE_REC_UPGRADE);
+    expect(dispatch).not.toHaveBeenCalled();
   });
 });
 
diff --git a/frontend/connectivity/__tests__/connect_device/status_checks_test.ts b/frontend/connectivity/__tests__/connect_device/status_checks_test.ts
index d3b060d9e..d79d86106 100644
--- a/frontend/connectivity/__tests__/connect_device/status_checks_test.ts
+++ b/frontend/connectivity/__tests__/connect_device/status_checks_test.ts
@@ -7,35 +7,46 @@ jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() }));
 import {
   onStatus,
   incomingStatus,
-  HACKY_FLAGS,
 } from "../../connect_device";
 import { slowDown } from "../../slow_down";
 import { fakeState } from "../../../__test_support__/fake_state";
 import { badVersion } from "../../../devices/actions";
+import { Actions } from "../../../constants";
 
 describe("onStatus()", () => {
-  const callOnStatus = (version: string | undefined) => {
-    HACKY_FLAGS.needVersionCheck = true;
-    const dispatch = jest.fn();
+  const callOnStatus = (version: string | undefined, dispatch: Function) => {
     const state = fakeState();
     state.bot.hardware.informational_settings.controller_version = version;
+    state.bot.needVersionCheck = true;
     onStatus(dispatch, () => state)(state.bot.hardware);
   };
 
   it("warns about old version", () => {
-    callOnStatus("0.0.0");
+    const dispatch = jest.fn();
+    callOnStatus("0.0.0", dispatch);
     expect(badVersion).toHaveBeenCalled();
+    expect(dispatch).toHaveBeenCalledWith({
+      type: Actions.SET_NEEDS_VERSION_CHECK, payload: false,
+    });
   });
 
   it("doesn't warn about old version", () => {
-    callOnStatus(undefined);
+    const dispatch = jest.fn();
+    callOnStatus(undefined, dispatch);
     expect(badVersion).not.toHaveBeenCalled();
+    expect(dispatch).not.toHaveBeenCalledWith({
+      type: Actions.SET_NEEDS_VERSION_CHECK, payload: false,
+    });
   });
 
   it("version ok", () => {
+    const dispatch = jest.fn();
     globalConfig.MINIMUM_FBOS_VERSION = "1.0.0";
-    callOnStatus("1.0.0");
+    callOnStatus("1.0.0", dispatch);
     expect(badVersion).not.toHaveBeenCalled();
+    expect(dispatch).toHaveBeenCalledWith({
+      type: Actions.SET_NEEDS_VERSION_CHECK, payload: false,
+    });
     delete globalConfig.MINIMUM_FBOS_VERSION;
   });
 
diff --git a/frontend/devices/__tests__/reducer_test.ts b/frontend/devices/__tests__/reducer_test.ts
index aa3289d8f..d8f73a8ed 100644
--- a/frontend/devices/__tests__/reducer_test.ts
+++ b/frontend/devices/__tests__/reducer_test.ts
@@ -135,4 +135,22 @@ describe("botReducer", () => {
     const step3 = botReducer(step2, no);
     expect(statusOf(step3)).toBe(step3.statusStash);
   });
+
+  it("sets needs version check flag", () => {
+    const state = initialState();
+    state.needVersionCheck = true;
+    const action = { type: Actions.SET_NEEDS_VERSION_CHECK, payload: false };
+    const r = botReducer(state, action);
+    expect(r.needVersionCheck).toEqual(false);
+  });
+
+  it("sets sent malformed message notification flag", () => {
+    const state = initialState();
+    state.alreadyToldUserAboutMalformedMsg = true;
+    const action = {
+      type: Actions.SET_MALFORMED_NOTIFICATION_SENT, payload: false,
+    };
+    const r = botReducer(state, action);
+    expect(r.alreadyToldUserAboutMalformedMsg).toEqual(false);
+  });
 });
diff --git a/frontend/devices/connectivity/__tests__/fbos_metric_history_plot_test.tsx b/frontend/devices/connectivity/__tests__/fbos_metric_history_plot_test.tsx
index a923b40fe..1b2a6bfa2 100644
--- a/frontend/devices/connectivity/__tests__/fbos_metric_history_plot_test.tsx
+++ b/frontend/devices/connectivity/__tests__/fbos_metric_history_plot_test.tsx
@@ -12,11 +12,16 @@ describe("<FbosMetricHistoryPlot />", () => {
     const telemetry1 = fakeTelemetry();
     telemetry1.body.created_at = 1;
     telemetry1.body.cpu_usage = undefined;
+    telemetry1.body.fbos_version = "1.2.3";
     const telemetry2 = fakeTelemetry();
     telemetry2.body.created_at = 2;
     telemetry2.body.throttled = undefined;
+    telemetry2.body.fbos_version = "4.5.6";
+    const telemetry3 = fakeTelemetry();
+    telemetry3.body.created_at = 3;
+    telemetry3.body.fbos_version = undefined;
     return {
-      telemetry: [telemetry0, telemetry1, telemetry2],
+      telemetry: [telemetry0, telemetry1, telemetry2, telemetry3],
       onHover: jest.fn(),
       hoveredMetric: undefined,
       hoveredTime: undefined,
diff --git a/frontend/password_reset/__tests__/password_reset_test.tsx b/frontend/password_reset/__tests__/password_reset_test.tsx
index f987b18f1..bd19bb265 100644
--- a/frontend/password_reset/__tests__/password_reset_test.tsx
+++ b/frontend/password_reset/__tests__/password_reset_test.tsx
@@ -24,14 +24,24 @@ describe("<PasswordReset/>", () => {
           }));
 
   it("handles form submission errors", async () => {
-    const pr = new PasswordReset({});
+    const wrapper = mount<PasswordReset>(<PasswordReset />);
     const e = formEvent();
-    pr.submit(e);
+    wrapper.instance().submit(e);
     expect(e.preventDefault).toHaveBeenCalled();
     await respondWith({ status: 400, response: { err: "xyz" } });
     expect(error).toHaveBeenCalledWith("Err: xyz");
   });
 
+  it("handles missing TOS agreement", async () => {
+    const pr = new PasswordReset({});
+    const e = formEvent();
+    pr.submit(e);
+    expect(e.preventDefault).toHaveBeenCalled();
+    await respondWith({ status: 451, response: { err: "xyz" } });
+    expect(error).not.toHaveBeenCalled();
+    expect(window.location.assign).toHaveBeenCalledWith("/tos_update");
+  });
+
   it("resets the users password", async () => {
     expect.assertions(5);
     const el = mount(<PasswordReset />);
diff --git a/frontend/settings/account/__tests__/change_password_test.tsx b/frontend/settings/account/__tests__/change_password_test.tsx
index 1da10f4d3..9286d0ce4 100644
--- a/frontend/settings/account/__tests__/change_password_test.tsx
+++ b/frontend/settings/account/__tests__/change_password_test.tsx
@@ -46,6 +46,19 @@ describe("<ChangePassword />", () => {
   });
 
   it("rejects new == old password case", () => {
+    const el = mount<ChangePassword>(<ChangePassword />);
+    el.instance().state.form = {
+      password: "password",
+      new_password: "password",
+      new_password_confirmation: "password"
+    };
+    el.instance().save();
+    const expectation = expect.stringContaining("Password not changed");
+    expect(error).toHaveBeenCalledWith(expectation);
+    expect(el.instance().state.status).toBe(SpecialStatus.SAVED);
+  });
+
+  it("rejects too short new password", () => {
     const el = mount<ChangePassword>(<ChangePassword />);
     el.instance().state.form = {
       password: "a",
@@ -53,7 +66,7 @@ describe("<ChangePassword />", () => {
       new_password_confirmation: "a"
     };
     el.instance().save();
-    const expectation = expect.stringContaining("Password not changed");
+    const expectation = expect.stringContaining("New password must be at least");
     expect(error).toHaveBeenCalledWith(expectation);
     expect(el.instance().state.status).toBe(SpecialStatus.SAVED);
   });
@@ -61,9 +74,9 @@ describe("<ChangePassword />", () => {
   it("rejects new != password confirmation case", () => {
     const el = mount<ChangePassword>(<ChangePassword />);
     el.instance().state.form = {
-      password: "a",
-      new_password: "b",
-      new_password_confirmation: "c"
+      password: "aaaaaaaa",
+      new_password: "bbbbbbbb",
+      new_password_confirmation: "cccccccc"
     };
     el.instance().save();
     const expectation = expect.stringContaining("do not match");
@@ -73,7 +86,12 @@ describe("<ChangePassword />", () => {
 
   it("throws a form error", () => {
     const el = mount<ChangePassword>(<ChangePassword />);
-    el.instance().state.form = {} as ChangePWState["form"];
+    el.instance().state.form = {
+      password: "password0",
+      new_password: "password1",
+      new_password_confirmation: "password2",
+      extra_password: "password3",
+    } as ChangePWState["form"];
     expect(el.instance().save).toThrowError("form error");
     expect(el.instance().state.status).toBe(SpecialStatus.SAVED);
   });
@@ -81,9 +99,9 @@ describe("<ChangePassword />", () => {
   it("cancels password change", () => {
     const el = mount<ChangePassword>(<ChangePassword />);
     el.instance().state.form = {
-      password: "a",
-      new_password: "b",
-      new_password_confirmation: "b"
+      password: "aaaaaaaa",
+      new_password: "bbbbbbbb",
+      new_password_confirmation: "bbbbbbbb"
     };
     window.confirm = () => false;
     el.instance().save();
@@ -110,9 +128,9 @@ describe("<ChangePassword />", () => {
       window.confirm = () => true;
       const el = mount<ChangePassword>(<ChangePassword />);
       el.instance().state.form = {
-        password: "x",
-        new_password: "b",
-        new_password_confirmation: "b"
+        password: "xxxxxxxx",
+        new_password: "bbbbbbbb",
+        new_password_confirmation: "bbbbbbbb"
       };
       el.instance().save();
       const resp = await respondWith({ status: 422, response: { bad: "data" } });
@@ -125,9 +143,9 @@ describe("<ChangePassword />", () => {
       window.confirm = () => true;
       const el = mount<ChangePassword>(<ChangePassword />);
       el.instance().state.form = {
-        password: "a",
-        new_password: "b",
-        new_password_confirmation: "b"
+        password: "aaaaaaaa",
+        new_password: "bbbbbbbb",
+        new_password_confirmation: "bbbbbbbb"
       };
       el.instance().save();
       const resp = await respondWith({ status: 200, response: {} });