Tests for auto_sync.ts
parent
9e5099142a
commit
7cc51178b9
|
@ -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"
|
||||
|
|
|
@ -29,6 +29,7 @@ describe("maybeRefreshToken()", () => {
|
|||
token: {
|
||||
encoded: "---",
|
||||
unencoded: {
|
||||
jti: "---",
|
||||
iss: "---",
|
||||
exp: 456,
|
||||
mqtt: "---",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const mockAuth = (iss = "987"): AuthState => ({
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: { iss, os_update_server: "---" }
|
||||
unencoded: { iss, os_update_server: "---", jti: "---" }
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue