Tests for auto_sync.ts

pull/527/head
Rick Carlino 2017-11-10 13:54:34 -06:00
parent 9e5099142a
commit 7cc51178b9
8 changed files with 180 additions and 27 deletions

View File

@ -3,6 +3,7 @@ import { AuthState } from "../../auth/interfaces";
export let auth: AuthState = {
"token": {
"unencoded": {
"jti": "xyz",
"iss": "//localhost:3000",
"os_update_server": "https://api.github.com/repos/farmbot/" +
"farmbot_os/releases/latest"

View File

@ -29,6 +29,7 @@ describe("maybeRefreshToken()", () => {
token: {
encoded: "---",
unencoded: {
jti: "---",
iss: "---",
exp: 456,
mqtt: "---",

View File

@ -1,7 +1,7 @@
const mockAuth = (iss = "987"): AuthState => ({
token: {
encoded: "---",
unencoded: { iss, os_update_server: "---" }
unencoded: { iss, os_update_server: "---", jti: "---" }
}
});

View File

@ -90,7 +90,7 @@ describe("didLogin()", () => {
const mockToken: AuthState = {
token: {
encoded: "---",
unencoded: { iss: "iss", os_update_server: "os_update_server" }
unencoded: { iss: "iss", os_update_server: "os_update_server", jti: "---" }
}
};
const dispatch = jest.fn();

View File

@ -10,10 +10,10 @@ export interface AuthState {
export interface UnencodedToken {
/** ISSUER - Where token came from (API URL). */
iss: string;
/** MQTT server address */
// mqtt: string;
/** Where to download RPi software */
os_update_server: string;
/** JSON Token Identifier- auto sync needs this to hear its echo on MQTT */
jti: string;
}
export interface User {

View File

@ -0,0 +1,154 @@
import {
SyncPayload,
decodeBinary,
routeMqttData,
Reason,
asTaggedResource,
UpdateMqttData,
handleCreate,
handleUpdate,
handleCreateOrUpdate
} from "../auto_sync";
import { SpecialStatus } from "../../resources/tagged_resources";
import { Actions } from "../../constants";
import { fakeState } from "../../__test_support__/fake_state";
import { GetState } from "../../redux/interfaces";
function toBinary(input: object): Buffer {
return Buffer.from(JSON.stringify(input), "utf8");
}
const fakePayload: SyncPayload = {
args: { label: "label1" },
body: { foo: "bar" }
};
const payload = (): UpdateMqttData => ({
status: "UPDATE",
kind: "Sequence",
id: 5,
body: {},
sessionId: "wow"
});
describe("handleCreateOrUpdate", () => {
it("creates new records if it doesn't have one locally", () => {
const myPayload = payload();
const dispatch = jest.fn();
const getState = jest.fn(fakeState);
const result = handleCreateOrUpdate(dispatch, getState, myPayload);
expect(result).toBe(undefined);
expect(dispatch).toHaveBeenCalled();
expect(dispatch.mock.calls[0][0].type).toBe(Actions.INIT_RESOURCE);
});
it("ignores local echo", () => {
jest.resetAllMocks();
const myPayload = payload();
const dispatch = jest.fn();
const getState = jest.fn(fakeState) as GetState;
const state = getState();
myPayload.sessionId = state.auth && state.auth.token.unencoded.jti || "X";
const result = handleCreateOrUpdate(dispatch, getState, myPayload);
expect(result).toBe(undefined);
expect(dispatch).not.toHaveBeenCalled();
});
it("updates existing records when found locally", () => {
const myPayload = payload();
const dispatch = jest.fn();
const getState = jest.fn(fakeState) as GetState;
const { index } = getState().resources;
const fakeId = Object.values(index.byKind.Sequence)[0].split(".")[1];
myPayload.id = parseInt(fakeId, 10);
myPayload.kind = "Sequence";
// const uuid = maybeDetermineUuid(index, myPayload.kind, myPayload.id);
// debugger;
handleCreateOrUpdate(dispatch, getState, myPayload);
expect(dispatch).toHaveBeenCalled();
expect(dispatch.mock.calls[0][0].type).toBe(Actions.OVERWRITE_RESOURCE);
});
});
describe("handleUpdate", () => {
it("creates Redux actions when data updates", () => {
const wow = handleUpdate(payload(), "whatever");
expect(wow.type).toEqual(Actions.OVERWRITE_RESOURCE);
});
});
describe("handleCreate", () => {
it("creates appropriate Redux actions", () => {
const wow = handleCreate(payload());
expect(wow.type).toEqual(Actions.INIT_RESOURCE);
});
});
describe("asTaggedResource", () => {
it("turns MQTT data into FE data", () => {
const UUID = "123-456-789";
const p = payload();
const result = asTaggedResource(p, UUID);
expect(result.body).toEqual(p.body);
expect(result.kind).toEqual(p.kind);
expect(result.specialStatus).toEqual(SpecialStatus.SAVED);
expect(result.uuid).toEqual(UUID);
});
});
describe("decodeBinary()", () => {
it("transforms binary back to JSON", () => {
const results = decodeBinary(toBinary(fakePayload));
expect(results.args).toBeInstanceOf(Object);
expect(results.args.label).toEqual("label1");
expect(results.body).toBeInstanceOf(Object);
});
});
describe("routeMqttData", () => {
it("tosses out irrelevant data", () => {
const results = routeMqttData("smething/else", toBinary({}));
expect(results.status).toEqual("SKIP");
});
it("tosses out data missing an ID", () => {
const results = routeMqttData("bot/device_9/sync", toBinary({}));
expect(results.status).toEqual("ERR");
results.status === "ERR" && expect(results.reason).toEqual(Reason.BAD_CHAN);
});
it("handles well formed deletion data", () => {
const results = routeMqttData("bot/device_9/sync/Sequence/1", toBinary({}));
expect(results.status).toEqual("DELETE");
if (results.status !== "DELETE") {
fail();
return;
}
expect(results.id).toEqual(1);
expect(results.kind).toEqual("Sequence");
});
it("handles well formed update data", () => {
const fake1 = {
args: {
label: "hey"
},
body: {
foo: "bar"
}
};
const payl = toBinary(fake1);
const results = routeMqttData("bot/device_9/sync/Sequence/1", payl);
expect(results.status).toEqual("UPDATE");
if (results.status !== "UPDATE") {
fail();
return;
}
expect(results.id).toEqual(1);
expect(results.kind).toEqual("Sequence");
expect(results.body).toEqual(fake1.body);
});
});

View File

@ -8,7 +8,7 @@ const mockRedux = {
jest.mock("../../redux/store", () => mockRedux);
jest.mock("lodash", () => {
return {
debounce: (x: Function) => x
throttle: (x: Function) => x
};
});
import { dispatchNetworkUp, dispatchNetworkDown } from "../index";

View File

@ -5,7 +5,7 @@ import { destroyOK } from "../resources/actions";
import { overwrite, init } from "../api/crud";
import { fancyDebug } from "../util";
interface UpdateMqttData {
export interface UpdateMqttData {
status: "UPDATE"
kind: ResourceName;
id: number;
@ -34,21 +34,22 @@ type MqttDataResult =
| SkipMqttData
| BadMqttData;
enum Reason {
export enum Reason {
BAD_KIND = "missing `kind`",
BAD_ID = "No ID or invalid ID.",
BAD_CHAN = "Expected exactly 5 segments in channel"
}
interface SyncPayload {
export interface SyncPayload {
args: { label: string; };
body: object | undefined;
}
function decodeBinary(payload: Buffer): SyncPayload {
export function decodeBinary(payload: Buffer): SyncPayload {
return JSON.parse((payload).toString());
}
function routeMqttData(chan: string, payload: Buffer): MqttDataResult {
export function routeMqttData(chan: string, payload: Buffer): MqttDataResult {
/** Skip irrelevant messages */
if (!chan.includes("sync")) { return { status: "SKIP" }; }
@ -57,12 +58,9 @@ function routeMqttData(chan: string, payload: Buffer): MqttDataResult {
if (parts.length !== 5) { return { status: "ERR", reason: Reason.BAD_CHAN }; }
const id = parseInt(parts.pop() || "0", 10);
const kind = parts.pop() as ResourceName | undefined;
const kind = parts.pop() as ResourceName;
const { body, args } = decodeBinary(payload);
if (!kind) { return { status: "ERR", reason: Reason.BAD_KIND }; }
if (!id) { return { status: "ERR", reason: Reason.BAD_ID }; }
if (body) {
return { status: "UPDATE", body, kind: kind, id, sessionId: args.label };
} else {
@ -70,7 +68,7 @@ function routeMqttData(chan: string, payload: Buffer): MqttDataResult {
}
}
const asTaggedResource = (data: UpdateMqttData, uuid: string): TaggedResource => {
export const asTaggedResource = (data: UpdateMqttData, uuid: string): TaggedResource => {
return {
// tslint:disable-next-line:no-any
kind: (data.kind as any),
@ -81,23 +79,22 @@ const asTaggedResource = (data: UpdateMqttData, uuid: string): TaggedResource =>
};
};
const handleCreate =
export const handleCreate =
(data: UpdateMqttData) => init(asTaggedResource(data, "IS SET LATER"), true);
const handleUpdate =
export const handleUpdate =
(d: UpdateMqttData, uid: string) => {
const tr = asTaggedResource(d, uid);
return overwrite(tr, tr.body, SpecialStatus.SAVED);
};
function handleCreateOrUpdate(dispatch: Function,
export function handleCreateOrUpdate(dispatch: Function,
getState: GetState,
data: UpdateMqttData,
backoff = 200) {
data: UpdateMqttData) {
const state = getState();
const { index } = state.resources;
const uuid = maybeDetermineUuid(index, data.kind, data.id);
if (uuid) {
return dispatch(handleUpdate(data, uuid));
} else {
@ -112,13 +109,13 @@ function handleCreateOrUpdate(dispatch: Function,
// The ultimate problem: We need to know if the incoming data update was created
// by us or some other user. That information lets us know if we are UPDATEing
// data or INSERTing data.
const jti: string =
(state.auth && (state.auth.token.unencoded as any)["jti"]) || "";
if (data.sessionId !== jti) { // Ignores local echo.
console.log("Acting on sync data");
dispatch(handleCreate(data));
} else {
const jti = state.auth && state.auth.token.unencoded.jti;
debugger;
if (data.sessionId === jti) { // Ignore local echo?
console.log("Ignoring echo");
} else {
dispatch(handleCreate(data));
console.log("Acting on sync data");
}
}
}