Merge pull request #1116 from FarmBot/staging

v7.2.1 - Happy Hibiscus Recovery Release
pull/1119/head
Rick Carlino 2019-02-13 14:12:47 -06:00 committed by GitHub
commit 73c35ed4f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 223 additions and 250 deletions

View File

@ -2,12 +2,15 @@ class SendNervesHubInfoJob < ApplicationJob
queue_as :default
def perform(device_id:, serial_number:, tags:)
device = Device.find(device_id)
DeviceSerialNumber.transaction do
device.update_attributes!(serial_number: serial_number)
resp_data = NervesHub.create_or_update(serial_number, tags)
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
Transport.current.amqp_send(certs.to_json, device_id, "nerves_hub")
end
device = Device.find(device_id)
resp_data = NervesHub.create_or_update(serial_number, tags)
certs = NervesHub.sign_device(resp_data.fetch(:identifier))
Transport.current.amqp_send(certs.to_json, device_id, "nerves_hub")
rescue => error
NervesHub.report_problem({ error: error,
device_id: device_id,
serial_number: serial_number,
tags: tags, })
raise error
end
end

View File

@ -298,4 +298,9 @@ private
def self.current_ca_file
@current_ca_file ||= (try_env_ca_file || try_file_ca_file || nil)
end
WHOOPS = "🚨 NervesHub Anomaly Detected! 🚨"
def self.report_problem(payload = {})
Rollbar.error(WHOOPS, payload)
end
end

View File

@ -32,7 +32,6 @@ class Device < ApplicationRecord
has_many :diagnostic_dumps, dependent: :destroy
has_many :fragments, dependent: :destroy
has_one :fbos_config, dependent: :destroy
has_one :device_serial_number, dependent: :destroy
has_many :in_use_tools
has_many :in_use_points
has_many :users

View File

@ -1,4 +0,0 @@
class DeviceSerialNumber < ApplicationRecord
belongs_to :device
# DO NOT USE THIS TABLE. IT IS DEPRECATED. DESTROY FEB 2019.
end

View File

@ -13,7 +13,15 @@ class FbosConfig < ApplicationRecord
def sync_nerves
serial = device.serial_number
return unless serial
unless serial
# This feature can be removed in May '19
# It is used to repair data damage on
# production during the initial nervehub
# deployment.
problem = "Device #{device.id} missing serial"
NervesHub.report_problem({ problem: problem })
return
end
self.delay.push_changes_to_nerves_hub(serial, update_channel)
end

View File

@ -10,7 +10,7 @@ module DeviceCerts
SendNervesHubInfoJob.perform_later(device_id: device.id,
serial_number: serial_number,
tags: tags)
return {}
return device.update_attributes!(serial_number: serial_number) && device
end
end
end

View File

@ -1,8 +1,16 @@
class DeprecateDeviceSerialNumberTable < ActiveRecord::Migration[5.2]
unless Kernel.const_defined?("DeviceSerialNumber")
# Shim so that legacy users don't crash when (up|down)grading
class DeviceSerialNumber
def self.preload(*x)
end
end
end
def change
DeviceSerialNumber.preload(:devices) do |x|
x.device
.update_attributes!(serial_number: x.serial_number)
x.device.update_attributes!(serial_number: x.serial_number)
end
end
end

View File

@ -0,0 +1,5 @@
class RemoveDeviceSerialNumberTable < ActiveRecord::Migration[5.2]
def change
drop_table :device_serial_numbers
end
end

View File

@ -14,7 +14,6 @@ if Rails.env == "development"
[
Sensor,
Peripheral,
DeviceSerialNumber,
Log,
PinBinding,
Point,

View File

@ -153,38 +153,6 @@ CREATE SEQUENCE public.delayed_jobs_id_seq
ALTER SEQUENCE public.delayed_jobs_id_seq OWNED BY public.delayed_jobs.id;
--
-- Name: device_serial_numbers; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.device_serial_numbers (
id bigint NOT NULL,
device_id bigint,
serial_number character varying(16) NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
--
-- Name: device_serial_numbers_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.device_serial_numbers_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: device_serial_numbers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.device_serial_numbers_id_seq OWNED BY public.device_serial_numbers.id;
--
-- Name: devices; Type: TABLE; Schema: public; Owner: -
--
@ -385,8 +353,7 @@ CREATE TABLE public.farmware_installations (
url character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
package character varying(80),
package_error character varying
package character varying(80)
);
@ -1580,13 +1547,6 @@ ALTER TABLE ONLY public.arg_sets ALTER COLUMN id SET DEFAULT nextval('public.arg
ALTER TABLE ONLY public.delayed_jobs ALTER COLUMN id SET DEFAULT nextval('public.delayed_jobs_id_seq'::regclass);
--
-- Name: device_serial_numbers id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.device_serial_numbers ALTER COLUMN id SET DEFAULT nextval('public.device_serial_numbers_id_seq'::regclass);
--
-- Name: devices id; Type: DEFAULT; Schema: public; Owner: -
--
@ -1850,14 +1810,6 @@ ALTER TABLE ONLY public.delayed_jobs
ADD CONSTRAINT delayed_jobs_pkey PRIMARY KEY (id);
--
-- Name: device_serial_numbers device_serial_numbers_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.device_serial_numbers
ADD CONSTRAINT device_serial_numbers_pkey PRIMARY KEY (id);
--
-- Name: devices devices_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -2151,13 +2103,6 @@ CREATE INDEX index_arg_sets_on_fragment_id ON public.arg_sets USING btree (fragm
CREATE INDEX index_arg_sets_on_node_id ON public.arg_sets USING btree (node_id);
--
-- Name: index_device_serial_numbers_on_device_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_device_serial_numbers_on_device_id ON public.device_serial_numbers USING btree (device_id);
--
-- Name: index_devices_on_mounted_tool_id; Type: INDEX; Schema: public; Owner: -
--
@ -2720,14 +2665,6 @@ ALTER TABLE ONLY public.edge_nodes
ADD CONSTRAINT fk_rails_c86213fd78 FOREIGN KEY (sequence_id) REFERENCES public.sequences(id);
--
-- Name: device_serial_numbers fk_rails_d052988096; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.device_serial_numbers
ADD CONSTRAINT fk_rails_d052988096 FOREIGN KEY (device_id) REFERENCES public.devices(id);
--
-- Name: points fk_rails_d6f3cdbe9a; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -2878,6 +2815,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20190103211708'),
('20190103213956'),
('20190108211419'),
('20190209133811');
('20190209133811'),
('20190212215842');

View File

@ -873,6 +873,9 @@ ul {
label {
margin-top: 5px;
}
input {
margin-bottom: 1rem;
}
}
.release-notes-button {

View File

@ -6,6 +6,7 @@
z-index: 3;
background: $black;
opacity: 0.9;
min-height: 3.25rem;
.bp3-collapse {
width: 100% !important;
overflow-y: scroll !important;

View File

@ -22,7 +22,7 @@
}
.widget-header {
height: 3.3rem;
min-height: 3.3rem;
background: $dark_gray;
letter-spacing: .05rem;
padding: .75rem 1rem;

View File

@ -22,7 +22,7 @@ import {
import { isString, isFunction } from "lodash";
import { repeatOptions } from "../map_state_to_props_add_edit";
import { SpecialStatus, VariableDeclaration } from "farmbot";
import { success, error } from "farmbot-toastr";
import { success, error, warning } from "farmbot-toastr";
import moment from "moment";
import { fakeState } from "../../../__test_support__/fake_state";
import { history } from "../../../history";
@ -336,8 +336,8 @@ describe("<FarmEventForm/>", () => {
p.farmEvent.body.end_time = "2017-05-22T06:00:00.000Z";
const i = instance(p);
await i.commitViewModel(moment("2017-06-22T05:00:00.000Z"));
expect(error).toHaveBeenCalledWith(expect.stringContaining(
"This Farm Event does not appear to have a valid run time"), "Warning");
expect(warning).toHaveBeenCalledWith(expect.stringContaining(
"Nothing to run."), "Warning");
});
it("rejects start time: add with unsupported OS", () => {

View File

@ -1,7 +1,7 @@
import * as React from "react";
import moment from "moment";
import { t } from "i18next";
import { success, error } from "farmbot-toastr";
import { success, error, warning } from "farmbot-toastr";
import {
TaggedFarmEvent, SpecialStatus, TaggedSequence, TaggedRegimen,
VariableDeclaration
@ -333,7 +333,7 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
/** Use the next item run time to display toast messages and return to
* the form if necessary. */
nextRunTimeActions = (editFEPath: string, now = moment()) => {
nextRunTimeActions = (now = moment()): boolean => {
const nextRun = this.nextItemTime(this.props.farmEvent.body, now);
if (nextRun) {
const nextRunText = this.props.autoSyncEnabled
@ -343,36 +343,36 @@ export class EditFEForm extends React.Component<EditFEProps, State> {
you must first SYNC YOUR DEVICE. If you do not sync, the event will
not run.`.replace(/\s+/g, " "), { timeFromNow: nextRun.from(now) });
success(nextRunText);
return true;
} else {
history.push(editFEPath);
error(t(Content.INVALID_RUN_TIME), t("Warning"));
warning(t("All items scheduled before the start time. Nothing to run."),
t("Warning"));
return false;
}
}
/** Once saved, if
* - Regimen Farm Event:
* * Return to calendar view.
* * If scheduled for today, warn about the possibility of missing tasks.
* * Display the start time difference from now and maybe prompt to sync.
* * Return to calendar view if items exist to be run past the start time.
* - Sequence Farm Event:
* * Determine the time for the next item to be run.
* * Return to calendar view only if more items exist to be run.
* * Display the next item run time.
* * If auto-sync is disabled, prompt the user to sync.
* * Return to calendar view only if more items exist to be run.
*/
commitViewModel = (now = moment()) => {
if (this.maybeRejectStartTime(this.updatedFarmEvent)) {
return startTimeWarning();
}
this.dispatch(overwrite(this.props.farmEvent, this.updatedFarmEvent));
const editFEPath = window.location.pathname;
this.dispatch(save(this.props.farmEvent.uuid))
.then(() => {
this.setState({ specialStatusLocal: SpecialStatus.SAVED });
history.push("/app/designer/farm_events");
this.dispatch(maybeWarnAboutMissedTasks(this.props.farmEvent,
() => alert(t(Content.REGIMEN_TODAY_SKIPPED_ITEM_RISK)), now));
this.nextRunTimeActions(editFEPath, now);
const itemsScheduled = this.nextRunTimeActions(now);
if (itemsScheduled) { history.push("/app/designer/farm_events"); }
})
.catch(() => {
error(t("Unable to save farm event."));

View File

@ -6,16 +6,16 @@ import * as React from "react";
import { mount } from "enzyme";
import {
seqDropDown,
initialValue,
InnerIf,
IfParams,
IfBlockDropDownHandler,
LHSOptions
LHSOptions,
ThenElseParams
} from "../index";
import {
buildResourceIndex, FAKE_RESOURCES
} from "../../../../__test_support__/resource_index_builder";
import { Execute, If, TaggedSequence } from "farmbot";
import { Execute, If, TaggedSequence, VariableDeclaration } from "farmbot";
import { overwrite } from "../../../../api/crud";
import {
fakeSensor, fakePeripheral
@ -59,13 +59,6 @@ describe("seqDropDown()", () => {
});
});
describe("initialValue()", () => {
it("returns dropdown initial value", () => {
const item = initialValue(execute, fakeResourceIndex);
expect(item).toEqual({ label: fakeName, value: fakeId });
});
});
describe("LHSOptions()", () => {
it("returns positions and pins", () => {
const s = fakeSensor();
@ -106,8 +99,13 @@ describe("<InnerIf />", () => {
});
describe("IfBlockDropDownHandler()", () => {
const fakeThenElseProps = (thenElseKey: "_then" | "_else"): ThenElseParams => ({
...fakeProps(),
thenElseKey,
});
it("onChange()", () => {
const { onChange } = IfBlockDropDownHandler(fakeProps(), "_else");
const { onChange } = IfBlockDropDownHandler(fakeThenElseProps("_else"));
onChange(expectedItem);
expect(overwrite).toHaveBeenCalledWith(
@ -135,16 +133,32 @@ describe("IfBlockDropDownHandler()", () => {
});
it("selectedItem()", () => {
const p = fakeProps();
const p = fakeThenElseProps("_then");
p.currentStep.args._then = execute;
const { selectedItem } = IfBlockDropDownHandler(p, "_then");
const { selectedItem } = IfBlockDropDownHandler(p);
const item = selectedItem();
expect(item).toEqual(expectedItem);
});
it("selectedItem(): null", () => {
const { selectedItem } = IfBlockDropDownHandler(fakeProps(), "_then");
const { selectedItem } = IfBlockDropDownHandler(fakeThenElseProps("_then"));
const item = selectedItem();
expect(item).toEqual({ label: "None", value: "" });
});
it("edits declarations", () => {
const declaration: VariableDeclaration = {
kind: "variable_declaration",
args: {
label: "label", data_value: {
kind: "coordinate", args: { x: 0, y: 0, z: 0 }
}
}
};
const p = fakeThenElseProps("_then");
const { assignVariable } =
IfBlockDropDownHandler(p);
assignVariable([])(declaration);
expect(p.currentStep.args._then.body).toEqual([declaration]);
});
});

View File

@ -1,13 +1,13 @@
import * as React from "react";
import { Else } from "../else";
import { ThenElse } from "../then_else";
import { mount } from "enzyme";
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
import { If } from "farmbot/dist";
import { IfParams } from "../index";
import { ThenElseParams } from "../index";
import { emptyState } from "../../../../resources/reducer";
describe("<Else/>", () => {
function fakeProps(): IfParams {
describe("<ThenElse/>", () => {
function fakeProps(): ThenElseParams {
const currentStep: If = {
kind: "_if",
args: {
@ -25,11 +25,21 @@ describe("<Else/>", () => {
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
thenElseKey: "_then",
};
}
it("renders", () => {
const wrapper = mount(<Else {...fakeProps()} />);
it("renders 'then'", () => {
const wrapper = mount(<ThenElse {...fakeProps()} />);
["THEN", "Execute Sequence"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.find("button").length).toEqual(1);
});
it("renders 'else'", () => {
const p = fakeProps();
p.thenElseKey = "_else";
const wrapper = mount(<ThenElse {...p} />);
["ELSE", "Execute Sequence"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.find("button").length).toEqual(1);

View File

@ -1,37 +0,0 @@
import * as React from "react";
import { Then } from "../then";
import { mount } from "enzyme";
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
import { If } from "farmbot/dist";
import { IfParams } from "../index";
import { emptyState } from "../../../../resources/reducer";
describe("<Then/>", () => {
function fakeProps(): IfParams {
const currentStep: If = {
kind: "_if",
args: {
lhs: "pin0",
op: "is",
rhs: 0,
_then: { kind: "nothing", args: {} },
_else: { kind: "nothing", args: {} }
}
};
return {
currentSequence: fakeSequence(),
currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
};
}
it("renders", () => {
const wrapper = mount(<Then {...fakeProps()} />);
["THEN", "Execute Sequence"].map(string =>
expect(wrapper.text()).toContain(string));
expect(wrapper.find("button").length).toEqual(1);
});
});

View File

@ -1,23 +0,0 @@
import * as React from "react";
import { IfParams, seqDropDown, IfBlockDropDownHandler } from "./index";
import { t } from "i18next";
import { Row, Col, FBSelect } from "../../../ui/index";
export function Else(props: IfParams) {
const { onChange, selectedItem } = IfBlockDropDownHandler(props, "_else");
return <Row>
<Col xs={12} md={12}>
<h4>{t("ELSE...")}</h4>
</Col>
<Col xs={12} md={12}>
<label>{t("Execute Sequence")}</label>
<FBSelect
key={JSON.stringify(props.currentSequence)}
allowEmpty={true}
list={seqDropDown(props.resources)}
placeholder="Sequence..."
onChange={onChange}
selectedItem={selectedItem()} />
</Col>
</Row>;
}

View File

@ -1,14 +1,13 @@
import * as React from "react";
import { t } from "i18next";
import { DropDownItem, NULL_CHOICE } from "../../../ui/index";
import { TaggedSequence } from "farmbot";
import { TaggedSequence, VariableDeclaration } from "farmbot";
import { If, Execute, Nothing } from "farmbot/dist";
import { ResourceIndex } from "../../../resources/interfaces";
import { selectAllSequences, findSequenceById } from "../../../resources/selectors";
import { isRecursive } from "../index";
import { If_ } from "./if";
import { Then } from "./then";
import { Else } from "./else";
import { ThenElse } from "./then_else";
import { defensiveClone } from "../../../util";
import { overwrite } from "../../../api/crud";
import { ToolTips } from "../../../constants";
@ -18,6 +17,7 @@ import {
} from "../pin_and_peripheral_support";
import { ShouldDisplay, Feature } from "../../../devices/interfaces";
import { isNumber, isString } from "lodash";
import { addOrEditVarDeclaration } from "../../locals_list/declaration_support";
export interface IfParams {
currentSequence: TaggedSequence;
@ -29,6 +29,10 @@ export interface IfParams {
confirmStepDeletion: boolean;
}
export interface ThenElseParams extends IfParams {
thenElseKey: "_then" | "_else";
}
export type Operator = "lhs"
| "op"
| "rhs"
@ -67,23 +71,6 @@ export function seqDropDown(i: ResourceIndex) {
return results;
}
export function initialValue(input: Execute | Nothing, index: ResourceIndex) {
switch (input.kind) {
case "execute":
const id = input.args.sequence_id;
const seq = findSequenceById(index, id).body;
if (isNumber(seq.id)) {
return { label: seq.name, value: seq.id };
} else {
throw new Error("Failed seq id type assertion.");
}
case "nothing":
return { label: t("None"), value: 0 };
default:
throw new Error("Only _else or _then");
}
}
export function InnerIf(props: IfParams) {
const {
index,
@ -103,17 +90,17 @@ export function InnerIf(props: IfParams) {
dispatch={dispatch}
index={index}
confirmStepDeletion={confirmStepDeletion}>
{recursive && (
{recursive &&
<span>
<i className="fa fa-exclamation-triangle"></i>
&nbsp;{t("Recursive condition.")}
</span>
)}
}
</StepHeader>
<StepContent className={className}>
<If_ {...props} />
<Then {...props} />
<Else {...props} />
<ThenElse thenElseKey={"_then"} {...props} />
<ThenElse thenElseKey={"_else"} {...props} />
</StepContent>
</StepWrapper>;
}
@ -121,13 +108,12 @@ export function InnerIf(props: IfParams) {
/** Creates a function that can be used in the `onChange` event of a _else or
* _then block in the sequence editor.
*/
export let IfBlockDropDownHandler = (props: IfParams,
key: "_else" | "_then") => {
export let IfBlockDropDownHandler = (props: ThenElseParams) => {
const { dispatch, index } = props;
const { dispatch, index, thenElseKey } = props;
const step = props.currentStep;
const sequence = props.currentSequence;
const block = step.args[key];
const block = step.args[thenElseKey];
const selectedItem = () => {
if (block.kind === "nothing") {
return NULL_CHOICE;
@ -145,7 +131,7 @@ export let IfBlockDropDownHandler = (props: IfParams,
function overwriteStep(input: Execute | Nothing) {
const update = defensiveClone(step);
const nextSequence = defensiveClone(sequence).body;
update.args[key] = input;
update.args[thenElseKey] = input;
(nextSequence.body || [])[index] = update;
dispatch(overwrite(sequence, nextSequence));
}
@ -159,5 +145,18 @@ export let IfBlockDropDownHandler = (props: IfParams,
}
}
return { onChange, selectedItem };
const sequenceId = selectedItem().value;
const calleeUuid = sequenceId ?
findSequenceById(props.resources, sequenceId).uuid : undefined;
const calledSequenceVariableData = calleeUuid ?
props.resources.sequenceMetas[calleeUuid] : undefined;
/** Replaces the execute step body with a new array of declarations. */
const assignVariable = (declarations: VariableDeclaration[]) =>
(variable: VariableDeclaration) => {
block.body = addOrEditVarDeclaration(declarations, variable);
overwriteStep(block);
};
return { onChange, selectedItem, calledSequenceVariableData, assignVariable };
};

View File

@ -1,23 +0,0 @@
import * as React from "react";
import { IfParams, seqDropDown, IfBlockDropDownHandler } from "./index";
import { t } from "i18next";
import { Row, Col, FBSelect } from "../../../ui/index";
export function Then(props: IfParams) {
const { onChange, selectedItem } = IfBlockDropDownHandler(props, "_then");
return <Row>
<Col xs={12} md={12}>
<h4>{t("THEN...")}</h4>
</Col>
<Col xs={12} md={12}>
<label>{t("Execute Sequence")}</label>
<FBSelect
key={JSON.stringify(props.currentSequence)}
allowEmpty={true}
list={seqDropDown(props.resources)}
placeholder="Sequence..."
onChange={onChange}
selectedItem={selectedItem()} />
</Col>
</Row>;
}

View File

@ -0,0 +1,40 @@
import * as React from "react";
import { ThenElseParams, seqDropDown, IfBlockDropDownHandler } from "./index";
import { t } from "i18next";
import { Row, Col, FBSelect } from "../../../ui";
import { LocalsList } from "../../locals_list/locals_list";
import { AllowedDeclaration } from "../../locals_list/locals_list_support";
export function ThenElse(props: ThenElseParams) {
const {
onChange, selectedItem, calledSequenceVariableData, assignVariable
} = IfBlockDropDownHandler(props);
const { body } = props.currentStep.args[props.thenElseKey];
return <Row>
<Col xs={12} md={12}>
<h4>{props.thenElseKey === "_then" ? t("THEN...") : t("ELSE...")}</h4>
</Col>
<Col xs={12} md={12}>
<label>{t("Execute Sequence")}</label>
<FBSelect
key={JSON.stringify(props.currentSequence)}
allowEmpty={true}
list={seqDropDown(props.resources)}
placeholder="Sequence..."
onChange={onChange}
selectedItem={selectedItem()} />
{!!calledSequenceVariableData &&
<Col xs={12}>
<LocalsList
declarations={body}
variableData={calledSequenceVariableData}
sequenceUuid={props.currentSequence.uuid}
resources={props.resources}
onChange={assignVariable(body || [])}
locationDropdownKey={JSON.stringify(props.currentSequence)}
allowedDeclarations={AllowedDeclaration.identifier}
shouldDisplay={props.shouldDisplay || (() => false)} />
</Col>}
</Col>
</Row>;
}

View File

@ -34,7 +34,6 @@ describe Api::DeviceCertsController do
post_args = ["/orgs/farmbot/devices/456/certificates/sign",
post_data,
{"Content-Type"=>"application/json"}]
old_count = DeviceSerialNumber.count
# Setup wiring ===========================================================
NervesHub.set_conn(conn)
@ -57,8 +56,8 @@ describe Api::DeviceCertsController do
post :create, body: payl.to_json, params: {format: :json}
end
expect(response.status).to eq(200)
expect(json).to eq({})
expect(DeviceSerialNumber.count).to eq(old_count)
expect(json).to be_kind_of(Hash)
expect(json.fetch(:id)).to eq(device.id)
end
end
end

View File

@ -1,6 +0,0 @@
FactoryBot.define do
factory :device_serial_number do
device { nil }
serial_number { raise "Stop using this model" }
end
end

View File

@ -0,0 +1,21 @@
require "spec_helper"
describe SendNervesHubInfoJob do
let(:device) { FactoryBot.create(:device) }
it "handles failure" do
params = { device_id: device.id,
serial_number: "xyz",
tags: [],
error: StandardError.new("Hello!"), }
not_work = \
receive(:create_or_update).with(any_args).and_raise(params.fetch(:error))
expect(NervesHub).to not_work
old_logger = ActiveJob::Base.logger
ActiveJob::Base.logger = Logger.new(nil)
expect do
SendNervesHubInfoJob.perform_now(**params.except(:error))
end.to raise_error(params.fetch(:error))
ActiveJob::Base.logger = old_logger
end
end

View File

@ -1,17 +1,31 @@
require 'spec_helper'
require "spec_helper"
describe FbosConfig do
let(:device) { FactoryBot.create(:device) }
let(:config) { FbosConfig.create!(device: device) }
let(:device) { FactoryBot.create(:device) }
let(:config) { FbosConfig.create!(device: device) }
it 'triggers callbacks' do
conn = double("Create a cert", :ca_file= => nil,
:cert_store => nil,
:cert_store= => nil,
:use_ssl => nil,
:use_ssl= => nil,
:cert= => nil,
:key= => nil)
def fake_conn(desc)
double(desc, :ca_file= => nil,
:cert_store => nil,
:cert_store= => nil,
:use_ssl => nil,
:use_ssl= => nil,
:cert= => nil,
:key= => nil)
end
it "notifies us of broken production data" do
# Remove this test by May 2019.
config.device.update_attributes!(serial_number: nil)
conn = fake_conn("Report broke data")
NervesHub.set_conn(conn)
problem = "Device #{device.id} missing serial"
expect(NervesHub).to receive(:report_problem).with({ problem: problem })
config.sync_nerves
end
it "triggers callbacks" do
conn = fake_conn("Create a cert")
NervesHub.set_conn(conn)
url = "/orgs/farmbot/devices/#{device.serial_number}"
resp = StubResp.new("200", { "data" => { "tags": [] } }.to_json)

View File

@ -33,7 +33,7 @@ describe DeviceCerts::Create do
result = DeviceCerts::Create.run!(tags: tags,
device: device,
serial_number: ser)
expect(result).to eq({})
expect(result).to eq(device)
end
end
end