A few remaining unit tests. Ready to go structurally
parent
9dfc1c52b9
commit
3a95f86714
|
@ -23,7 +23,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
|
|||
dispatch(fetchReleases(authState.token.unencoded.os_update_server));
|
||||
dispatch(loginOk(authState));
|
||||
Sync.fetchSyncData(dispatch);
|
||||
dispatch(connectDevice(authState.token.encoded));
|
||||
dispatch(connectDevice(authState));
|
||||
}
|
||||
|
||||
// We need to handle OK logins for numerous use cases (Ex: login & registration)
|
||||
|
|
|
@ -5,12 +5,24 @@ jest.mock("farmbot-toastr", () => ({
|
|||
warning: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../index", () => ({
|
||||
dispatchNetworkUp: jest.fn()
|
||||
}));
|
||||
|
||||
import { HardwareState } from "../../../devices/interfaces";
|
||||
import { incomingStatus, ifToastWorthy, showLogOnScreen, TITLE } from "../../connect_device";
|
||||
import {
|
||||
incomingStatus,
|
||||
ifToastWorthy,
|
||||
showLogOnScreen,
|
||||
TITLE,
|
||||
bothUp,
|
||||
initLog
|
||||
} from "../../connect_device";
|
||||
import { Actions } from "../../../constants";
|
||||
import { Log } from "../../../interfaces";
|
||||
import { ALLOWED_CHANNEL_NAMES, ALLOWED_MESSAGE_TYPES } from "farmbot";
|
||||
import { success, error, info } from "farmbot-toastr";
|
||||
import { dispatchNetworkUp } from "../../index";
|
||||
|
||||
describe("incomingStatus", () => {
|
||||
it("creates an action", () => {
|
||||
|
@ -69,3 +81,25 @@ describe("showLogOnScreen", () => {
|
|||
assertToastr(["success"], success);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initLog", () => {
|
||||
it("creates a Redux action (new log)", () => {
|
||||
const log = fakeLog("error");
|
||||
const action = initLog(log);
|
||||
expect(action.payload.kind).toBe("logs");
|
||||
// expect(action.payload.specialStatus).toBe(undefined);
|
||||
if (action.payload.kind === "logs") {
|
||||
expect(action.payload.body.message).toBe(log.message);
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bothUp()", () => {
|
||||
it("marks MQTT and API as up", () => {
|
||||
bothUp();
|
||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("user.mqtt");
|
||||
expect(dispatchNetworkUp).toHaveBeenCalledWith("bot.mqtt");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
import { setDevice, getDevice } from "../device";
|
||||
import { fetchNewDevice, getDevice } from "../device";
|
||||
import { dispatchNetworkUp, dispatchNetworkDown } from "./index";
|
||||
import { Log } from "../interfaces";
|
||||
import { ALLOWED_CHANNEL_NAMES, Farmbot, BotStateTree } from "farmbot";
|
||||
import { get, throttle, noop } from "lodash";
|
||||
import { success, error, info, warning } from "farmbot-toastr";
|
||||
import { HardwareState } from "../devices/interfaces";
|
||||
import { GetState } from "../redux/interfaces";
|
||||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { GetState, ReduxAction } from "../redux/interfaces";
|
||||
import { Content, Actions } from "../constants";
|
||||
import { t } from "i18next";
|
||||
import { isLog, EXPECTED_MAJOR, EXPECTED_MINOR, commandOK, badVersion } from "../devices/actions";
|
||||
import { oneOf, bail } from "../util";
|
||||
import {
|
||||
isLog,
|
||||
EXPECTED_MAJOR,
|
||||
EXPECTED_MINOR,
|
||||
commandOK,
|
||||
badVersion
|
||||
} from "../devices/actions";
|
||||
import { init } from "../api/crud";
|
||||
import { versionOK } from "../devices/reducer";
|
||||
import { Token } from "../auth/interfaces";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
import { TaggedResource } from "../resources/tagged_resources";
|
||||
|
||||
/** Welcome to one of the oldest pieces of FarmBot's software stack. */
|
||||
const AUTH_NOT_READY = "Somehow managed to get here before auth was ready.";
|
||||
const CHANNELS: keyof Log = "channels";
|
||||
const TOAST: ALLOWED_CHANNEL_NAMES = "toast";
|
||||
export const TITLE = "New message from bot";
|
||||
let NEED_VERSION_CHECK = true;
|
||||
// Already filtering messages in FarmBot OS and the API- this is just for
|
||||
// an additional layer of safety. If sensitive data ever hits a client, it will
|
||||
// be reported to Rollbar for investigation.
|
||||
type ConnectDeviceReturn = {} | ((dispatch: Function) => void);
|
||||
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
|
||||
let alreadyToldYou = false;
|
||||
const secure = location.protocol === "https:";
|
||||
|
||||
/** 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 */
|
||||
const HACKY_FLAGS = {
|
||||
needVersionCheck: true,
|
||||
alreadyToldUserAboutMalformedMsg: false
|
||||
};
|
||||
|
||||
/** Action creator that is called when FarmBot OS emits a status update.
|
||||
* Coordinate updates, movement, etc.*/
|
||||
export function incomingStatus(statusMessage: HardwareState) {
|
||||
return { type: Actions.BOT_CHANGE, payload: statusMessage };
|
||||
}
|
||||
export let incomingStatus = (statusMessage: HardwareState) =>
|
||||
({ type: Actions.BOT_CHANGE, payload: statusMessage });
|
||||
|
||||
/** Determine if an incoming log is a toast message. If it is, execute the
|
||||
* supplied callback */
|
||||
export function ifToastWorthy(log: Log | undefined,
|
||||
cb: (log: Log) => void) {
|
||||
const chanList = get(log || {}, CHANNELS, ["ERROR FETCHING CHANNELS"]);
|
||||
* supplied callback. */
|
||||
export function ifToastWorthy(log: Log, cb: (log: Log) => void) {
|
||||
const CHANNELS: keyof Log = "channels";
|
||||
const TOAST: ALLOWED_CHANNEL_NAMES = "toast";
|
||||
const chanList = get(log, CHANNELS, ["ERROR FETCHING CHANNELS"]);
|
||||
return log && chanList.includes(TOAST) ? cb(log) : noop();
|
||||
}
|
||||
|
||||
|
@ -60,10 +60,14 @@ export function showLogOnScreen(log: Log) {
|
|||
}
|
||||
}
|
||||
|
||||
const maybeShowLog =
|
||||
(mystery: Log | undefined) => ifToastWorthy(mystery, showLogOnScreen);
|
||||
export const initLog = (log: Log): ReduxAction<TaggedResource> => init({
|
||||
kind: "logs",
|
||||
specialStatus: undefined,
|
||||
uuid: "MUST_CHANGE",
|
||||
body: log
|
||||
});
|
||||
|
||||
const bothUp = () => {
|
||||
export const bothUp = () => {
|
||||
dispatchNetworkUp("user.mqtt");
|
||||
dispatchNetworkUp("bot.mqtt");
|
||||
};
|
||||
|
@ -90,14 +94,14 @@ const onStatus = (dispatch: Function, getState: GetState) =>
|
|||
(throttle(function (msg: BotStateTree) {
|
||||
bothUp();
|
||||
dispatch(incomingStatus(msg));
|
||||
if (NEED_VERSION_CHECK) {
|
||||
if (HACKY_FLAGS.needVersionCheck) {
|
||||
const IS_OK = versionOK(getState()
|
||||
.bot
|
||||
.hardware
|
||||
.informational_settings
|
||||
.controller_version, EXPECTED_MAJOR, EXPECTED_MINOR);
|
||||
if (!IS_OK) { badVersion(); }
|
||||
NEED_VERSION_CHECK = false;
|
||||
HACKY_FLAGS.needVersionCheck = false;
|
||||
}
|
||||
}, 500));
|
||||
|
||||
|
@ -109,14 +113,9 @@ const onSent = (/** The MQTT Client Object (bot.client) */ client: {}) =>
|
|||
|
||||
const onLogs = (dispatch: Function) => (msg: Log) => {
|
||||
bothUp();
|
||||
if (isLog(msg) && !oneOf(BAD_WORDS, msg.message.toUpperCase())) {
|
||||
maybeShowLog(msg);
|
||||
dispatch(init({
|
||||
kind: "logs",
|
||||
specialStatus: undefined,
|
||||
uuid: "MUST_CHANGE",
|
||||
body: msg
|
||||
}));
|
||||
if (isLog(msg)) {
|
||||
ifToastWorthy(msg, showLogOnScreen);
|
||||
dispatch(initLog(msg));
|
||||
// CORRECT SOLUTION: Give each device its own topic for publishing
|
||||
// MQTT last will message.
|
||||
// FAST SOLUTION: We would need to re-publish FBJS and FBOS to
|
||||
|
@ -126,48 +125,31 @@ const onLogs = (dispatch: Function) => (msg: Log) => {
|
|||
const died =
|
||||
msg.message.includes("is offline") && msg.meta.type === "error";
|
||||
died && dispatchNetworkDown("bot.mqtt");
|
||||
} else {
|
||||
throw new Error("Refusing to display log: " + JSON.stringify(msg));
|
||||
}
|
||||
};
|
||||
|
||||
function onMalformed() {
|
||||
bothUp();
|
||||
if (!alreadyToldYou) {
|
||||
if (!HACKY_FLAGS.alreadyToldUserAboutMalformedMsg) {
|
||||
warning(t(Content.MALFORMED_MESSAGE_REC_UPGRADE));
|
||||
alreadyToldYou = true;
|
||||
HACKY_FLAGS.alreadyToldUserAboutMalformedMsg = true;
|
||||
}
|
||||
}
|
||||
|
||||
const onOnline = () => dispatchNetworkUp("user.mqtt");
|
||||
|
||||
const attachEventListeners =
|
||||
(bot: Farmbot, dispatch: Function, getState: GetState) => () => {
|
||||
readStatus().then(changeLastClientConnected(bot), noop);
|
||||
bot.on("online", onOnline);
|
||||
bot.on("offline", onOffline);
|
||||
bot.on("sent", onSent(bot.client));
|
||||
bot.on("logs", onLogs(dispatch));
|
||||
bot.on("status", onStatus(dispatch, getState));
|
||||
bot.on("malformed", onMalformed);
|
||||
readStatus().then(changeLastClientConnected(bot), noop);
|
||||
};
|
||||
|
||||
const onOnline = () => dispatchNetworkUp("user.mqtt");
|
||||
|
||||
const doConnect = (dispatch: Function, getState: GetState) =>
|
||||
({ token }: { token: Token }) => {
|
||||
const bot = setDevice(new Farmbot({ token: token.encoded, secure }));
|
||||
return bot
|
||||
.connect()
|
||||
.then(attachEventListeners(bot, dispatch, getState), onOffline);
|
||||
};
|
||||
|
||||
export function connectDevice(oldToken: string): ConnectDeviceReturn {
|
||||
return (dispatch: Function, getState: GetState) => {
|
||||
const ath = getState().auth;
|
||||
const ok = doConnect(dispatch, getState);
|
||||
|
||||
ath ? (maybeRefreshToken(ath).then(ok)) : bail(AUTH_NOT_READY);
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Refresh the token, maybe.
|
||||
//
|
||||
/** Connect to MQTT and attach all relevant event handlers. */
|
||||
export let connectDevice = (token: AuthState) =>
|
||||
(dispatch: Function, getState: GetState) => fetchNewDevice(token)
|
||||
.then(bot => attachEventListeners(bot, dispatch, getState), onOffline);
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import { Farmbot } from "farmbot";
|
||||
import { bail } from "./util";
|
||||
import { set } from "lodash";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
import { maybeRefreshToken } from "./refresh_token";
|
||||
|
||||
let device: Farmbot;
|
||||
|
||||
const secure = location.protocol === "https:"; // :(
|
||||
|
||||
export const getDevice = (): Farmbot => (device || bail("NO DEVICE SET"));
|
||||
|
||||
export function setDevice(bot: Farmbot): Farmbot {
|
||||
set(window, "current_bot", bot);
|
||||
return device = bot;
|
||||
export function fetchNewDevice(oldToken: AuthState): Promise<Farmbot> {
|
||||
return maybeRefreshToken(oldToken)
|
||||
.then(({ token }) => {
|
||||
device = new Farmbot({ token: token.encoded, secure });
|
||||
set(window, "current_bot", device);
|
||||
return device;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { User } from "../auth/interfaces";
|
|||
import { getDeviceAccountSettings } from "../resources/selectors";
|
||||
import { TaggedDevice } from "../resources/tagged_resources";
|
||||
import { versionOK } from "./reducer";
|
||||
import { HttpData } from "../util";
|
||||
import { HttpData, oneOf } from "../util";
|
||||
import { Actions, Content } from "../constants";
|
||||
import { mcuParamValidator } from "./update_interceptor";
|
||||
|
||||
|
@ -27,9 +27,22 @@ const ON = 1, OFF = 0;
|
|||
export type ConfigKey = keyof McuParams;
|
||||
export const EXPECTED_MAJOR = 5;
|
||||
export const EXPECTED_MINOR = 0;
|
||||
// Already filtering messages in FarmBot OS and the API- this is just for
|
||||
// an additional layer of safety. If sensitive data ever hits a client, it will
|
||||
// be reported to Rollbar for investigation.
|
||||
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
|
||||
|
||||
export function isLog(x: object): x is Log {
|
||||
return _.isObject(x) && _.isString(_.get(x, "message" as keyof Log));
|
||||
// tslint:disable-next-line:no-any
|
||||
export function isLog(x: any): x is Log {
|
||||
const yup = _.isObject(x) && _.isString(_.get(x, "message" as keyof Log));
|
||||
if (yup) {
|
||||
if (oneOf(BAD_WORDS, x.message.toUpperCase())) {// SECURITY CRITICAL CODE.
|
||||
throw new Error("Refusing to display log: " + JSON.stringify(x));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const commandErr = (noun = "Command") => (x: {}) => {
|
||||
console.dir(x);
|
||||
|
|
|
@ -29,7 +29,7 @@ export let TickerList = (props: TickerListProps) => {
|
|||
const firstTicker: Log = props.logs.filter(
|
||||
log => !log.message.toLowerCase().includes("filtered"))[0];
|
||||
const noLogs: Log = {
|
||||
message: "No logs yet.", meta: { type: "success" }, channels: [], created_at: NaN
|
||||
message: "No logs yet.", meta: { type: "debug" }, channels: [], created_at: NaN
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import axios from "axios";
|
||||
import { API } from "./api/index";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
import { HttpData } from "./util";
|
||||
|
||||
export let maybeRefreshToken = (old: AuthState) => axios
|
||||
.get(API.current.tokensPath)
|
||||
.then(x => x.data, () => (old));
|
||||
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
|
||||
return axios
|
||||
.get(API.current.tokensPath)
|
||||
.then((x: HttpData<AuthState>) => x.data, () => (old));
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue