Merge conflicts
commit
b977825e0c
|
@ -13,6 +13,9 @@ module Api
|
|||
before_action :authenticate_user!
|
||||
skip_before_action :verify_authenticity_token
|
||||
after_action :skip_set_cookies_header
|
||||
|
||||
rescue_from(User::AlreadyVerified) { sorry "Already verified.", 409 }
|
||||
|
||||
rescue_from(JWT::VerificationError) { |e| auth_err }
|
||||
|
||||
rescue_from(ActionDispatch::Http::Parameters::ParseError) do
|
||||
|
@ -23,6 +26,7 @@ module Api
|
|||
rescue_from(ActiveRecord::ValueTooLong) do
|
||||
sorry "Please use reasonable lengths on string inputs", 422
|
||||
end
|
||||
|
||||
rescue_from Errors::Forbidden do |exc|
|
||||
sorry "You can't perform that action. #{exc.message}", 403
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# A human
|
||||
class User < ApplicationRecord
|
||||
class AlreadyVerified < StandardError; end
|
||||
ENFORCE_TOS = ENV.fetch("TOS_URL") { false }
|
||||
SKIP_EMAIL_VALIDATION = ENV.fetch("NO_EMAILS") { false }
|
||||
validates :email, uniqueness: true
|
||||
|
@ -32,4 +33,22 @@ class User < ApplicationRecord
|
|||
def verified?
|
||||
SKIP_EMAIL_VALIDATION ? true : !!confirmed_at
|
||||
end
|
||||
|
||||
BAD_SUB = "SUB was neither string nor number"
|
||||
class BadSub < StandardError; end # Safe to remove December '17 -RC
|
||||
def self.find_by_email_or_id(sub) # Safe to remove December '17 -RC
|
||||
case sub
|
||||
when Integer then User.find(sub)
|
||||
# HISTORICAL CONTEXT: We once used emails as a `sub` field. At the time,
|
||||
# it seemed nice because it was human readable. The problem was that
|
||||
# emails are mutable. Under this scheme, changing your email address
|
||||
# would invalidate your JWT. Switching it to user_id (that does not
|
||||
# change) gets around this issue. We still need to support emails in
|
||||
# JWTs, atleast for another month or so because it would invalidate
|
||||
# existing tokens otherwise.
|
||||
# TODO: Only use user_id (not email) for validation after 25 OCT 17 - RC
|
||||
when String then User.find_by!(email: sub)
|
||||
else raise BadSub, BAD_SUB
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,22 +7,9 @@ module Auth
|
|||
def execute
|
||||
token = SessionToken.decode!(just_the_token)
|
||||
claims = token.unencoded
|
||||
sub = claims["sub"]
|
||||
case sub
|
||||
when Integer then User.find(sub)
|
||||
# HISTORICAL CONTEXT: We once used emails as a `sub` field. At the time,
|
||||
# it seemed nice because it was human readable. The problem was that
|
||||
# emails are mutable. Under this scheme, changing your email address
|
||||
# would invalidate your JWT. Switching it to user_id (that does not
|
||||
# change) gets around this issue. We still need to support emails in
|
||||
# JWTs, atleast for another month or so because it would invalidate
|
||||
# existing tokens otherwise.
|
||||
# TODO: Only use user_id (not email) for validation after 25 OCT 17 - RC
|
||||
when String then User.find_by!(email: sub)
|
||||
else raise "SUB was neither string nor number"
|
||||
end
|
||||
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
|
||||
add_error :jwt, :decode_error, "JSON Web Token is not valid."
|
||||
User.find_by_email_or_id(claims["sub"])
|
||||
rescue JWT::DecodeError, ActiveRecord::RecordNotFound, User::BadSub
|
||||
add_error :jwt, :decode_error, Auth::ReloadToken::BAD_SUB
|
||||
end
|
||||
|
||||
def just_the_token
|
||||
|
|
|
@ -2,11 +2,17 @@ module Auth
|
|||
# The API supports a number of authentication strategies (Cookies, Bot token,
|
||||
# JWT). This service helps determine which auth strategy to use.
|
||||
class ReloadToken < Mutations::Command
|
||||
attr_reader :user
|
||||
BAD_SUB = "Please log out and try again."
|
||||
|
||||
required { string :jwt }
|
||||
|
||||
def validate
|
||||
@user = User.find_by_email_or_id(claims["sub"])
|
||||
end
|
||||
|
||||
def execute
|
||||
# Prevent never ending sessions.
|
||||
security_criticial_danger = claims["exp"]
|
||||
security_criticial_danger = claims["exp"] # Stop infinite sessions
|
||||
token = SessionToken.issue_to(user,
|
||||
aud: claims["aud"],
|
||||
exp: security_criticial_danger)
|
||||
|
@ -17,10 +23,6 @@ module Auth
|
|||
@claims ||= SessionToken.decode!(jwt.split(" ").last).unencoded
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= User.find(claims["sub"])
|
||||
end
|
||||
|
||||
def nope
|
||||
add_error :jwt, :decode_error, "JSON Web Token is not valid."
|
||||
end
|
||||
|
|
|
@ -3,17 +3,21 @@ module Users
|
|||
required { string :token, min_length: 5 }
|
||||
|
||||
def validate
|
||||
prevent_token_reuse
|
||||
end
|
||||
|
||||
def execute
|
||||
user.confirmed_at = Time.now
|
||||
# Prevent token reuse:
|
||||
user.confirmation_token = ""
|
||||
user.verified_at = Time.now
|
||||
user.save!
|
||||
SessionToken.as_json(user.reload, AbstractJwtToken::HUMAN_AUD)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prevent_token_reuse
|
||||
raise User::AlreadyVerified if user.verified_at.present?
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= User.find_by!(confirmation_token: token)
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ window.globalConfig = <%= raw($FRONTEND_SHARED_DATA) %> // SEE COMMENTS!;
|
|||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i" rel="stylesheet">
|
||||
<%= javascript_include_tag *webpack_asset_paths("commons") %>
|
||||
<% if false %>
|
||||
<script async src="http://<%= ENV["API_HOST"] %>:8080/target/target-script-min.js#anonymous">
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,7 @@ var devServerPort = 3808;
|
|||
var path = require("path");
|
||||
var genConfig = require("./webpack.base");
|
||||
var conf = genConfig();
|
||||
var webpack = require("webpack");
|
||||
|
||||
|
||||
conf.output = {
|
||||
// must match config.webpack.output_dir
|
||||
|
|
|
@ -19,7 +19,18 @@ conf.output = {
|
|||
[
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: "commons",
|
||||
async: true
|
||||
chunks: [
|
||||
"bundle",
|
||||
"front_page",
|
||||
"verification",
|
||||
"password_reset",
|
||||
"tos_update"
|
||||
],
|
||||
minChunks: ({ resource }) => (
|
||||
resource &&
|
||||
resource.indexOf('node_modules') >= 0 &&
|
||||
resource.match(/\.js$/)
|
||||
),
|
||||
}),
|
||||
new ExtractTextPlugin({
|
||||
// Temporary hotfix for some issues on staging.
|
||||
|
|
18
package.json
18
package.json
|
@ -29,10 +29,8 @@
|
|||
"@blueprintjs/core": "^1.29.0",
|
||||
"@blueprintjs/datetime": "^1.22.0",
|
||||
"@blueprintjs/labs": "^0.4.0",
|
||||
"@types/deep-freeze": "^0.1.1",
|
||||
"@types/enzyme": "^2.8.9",
|
||||
"@types/fastclick": "^1.0.28",
|
||||
"@types/handlebars": "^4.0.36",
|
||||
"@types/history": "^4.6.0",
|
||||
"@types/i18next": "^8.4.2",
|
||||
"@types/jest": "^21.1.2",
|
||||
|
@ -41,37 +39,34 @@
|
|||
"@types/moxios": "^0.4.5",
|
||||
"@types/mqtt": "^0.0.34",
|
||||
"@types/node": "^8.0.34",
|
||||
"@types/offline-js": "^0.7.28",
|
||||
"@types/react": "15.0.39",
|
||||
"@types/react-color": "^2.13.2",
|
||||
"@types/react-dom": "^15.5.5",
|
||||
"@types/react-redux": "^4.4.47",
|
||||
"@types/react-router": "3",
|
||||
"@types/react": "15.0.39",
|
||||
"@types/redux": "^3.6.31",
|
||||
"axios": "^0.16.2",
|
||||
"boxed_value": "^1.0.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"enzyme": "^2.9.1",
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"farmbot": "5.0.1-rc13",
|
||||
"farmbot-toastr": "^1.0.3",
|
||||
"farmbot": "5.0.1-rc12",
|
||||
"fastclick": "^1.0.6",
|
||||
"file-loader": "^1.1.5",
|
||||
"handlebars": "^4.0.10",
|
||||
"i18next": "^9.0.0",
|
||||
"imports-loader": "^0.7.0",
|
||||
"jest": "^21.2.1",
|
||||
"json-loader": "^0.5.7",
|
||||
"lodash": "^4.17.4",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"moment": "^2.19.1",
|
||||
"moxios": "^0.4.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"offline-js": "^0.7.19",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"react": "^15.6.1",
|
||||
"react-addons-css-transition-group": "^15.6.2",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
"react-color": "^2.13.8",
|
||||
|
@ -79,10 +74,9 @@
|
|||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-test-renderer": "^15.6.1",
|
||||
"react": "^15.6.1",
|
||||
"redux": "^3.7.2",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"redux": "^3.7.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"stats-webpack-plugin": "^0.6.1",
|
||||
"style-loader": "^0.19.0",
|
||||
|
@ -92,8 +86,8 @@
|
|||
"tslint": "^5.7.0",
|
||||
"typescript": "^2.5.3",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack-uglify-js-plugin": "^1.1.9",
|
||||
"webpack": "^3.7.1",
|
||||
"webpack-uglify-js-plugin": "^1.1.9",
|
||||
"weinre": "^2.0.0-pre-I0Z7U9OV",
|
||||
"which": "^1.3.0",
|
||||
"yarn": "^1.2.1"
|
||||
|
|
|
@ -30,7 +30,26 @@ describe Api::TokensController do
|
|||
it 'denies bad tokens' do
|
||||
request.headers["Authorization"] = "bearer #{auth_token.encoded + "no..."}"
|
||||
get :show
|
||||
expect(json.dig(:error, :jwt)).to include("not valid")
|
||||
expect(json.dig(:error, :jwt)).to eq(Auth::ReloadToken::BAD_SUB)
|
||||
end
|
||||
|
||||
it 'handles bad `sub` claims' do
|
||||
# Simulate a legacy API token.
|
||||
token = AbstractJwtToken.new([{
|
||||
aud: AbstractJwtToken::HUMAN_AUD,
|
||||
sub: "this_is_wrong@example.com",
|
||||
iat: Time.now.to_i,
|
||||
jti: SecureRandom.uuid,
|
||||
iss: "whatever",
|
||||
exp: (Time.now + 40.days).to_i,
|
||||
mqtt: "",
|
||||
mqtt_ws: "",
|
||||
os_update_server: "",
|
||||
bot: "device_#{user.device.id}" }])
|
||||
request.headers["Authorization"] = "bearer #{token.encoded}"
|
||||
get :show
|
||||
expect(response.status).to be(401)
|
||||
expect(json[:error].values).to include(Auth::ReloadToken::BAD_SUB)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -111,6 +111,13 @@ describe Api::UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
it 'can not re-verify' do
|
||||
user.update_attributes(verified_at: Time.now)
|
||||
sign_in user
|
||||
put :verify, params: { token: user.verification_token }, format: :json
|
||||
expect(response.status).to eq(409)
|
||||
end
|
||||
|
||||
it 'handles password confirmation mismatch' do
|
||||
email = Faker::Internet.email
|
||||
original_count = User.count
|
||||
|
|
|
@ -9,8 +9,8 @@ describe Api::UsersController do
|
|||
expect(user.confirmed_at).to eq(nil)
|
||||
put :verify, params: params
|
||||
user.reload
|
||||
expect(user.confirmation_token).to eq("")
|
||||
expect(user.confirmed_at).to be
|
||||
expect(user.confirmed_at - Time.now).to be < 3
|
||||
expect(user.verification_token).to eq(params[:token])
|
||||
expect(user.verified_at).to be
|
||||
expect(user.verified_at - Time.now).to be < 3
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,8 +38,10 @@ describe SessionToken do
|
|||
token = SessionToken.issue_to(user, iat: 000, exp: 1, iss: "//lycos.com:9867")
|
||||
result = Auth::FromJWT.run(jwt: token.encoded)
|
||||
expect(result.success?).to be(false)
|
||||
expect(result.errors.values.first.message).to include("is not valid")
|
||||
expect(result.errors.values.first.message)
|
||||
.to eq(Auth::ReloadToken::BAD_SUB)
|
||||
end
|
||||
|
||||
unless ENV["NO_EMAILS"]
|
||||
it "doesn't mint tokens for unverified users" do
|
||||
user.update_attributes!(confirmed_at: nil)
|
||||
|
|
|
@ -26,8 +26,8 @@ describe Auth::FromJWT do
|
|||
end
|
||||
|
||||
it "crashes when sub is neither string nor Integer" do
|
||||
t = fake[1.23]
|
||||
bad_sub = "SUB was neither string nor number"
|
||||
expect { Auth::FromJWT.run!(jwt: t) }.to raise_error(bad_sub)
|
||||
expect {
|
||||
Auth::FromJWT.run!(jwt: fake[1.23])
|
||||
}.to raise_error(Auth::ReloadToken::BAD_SUB)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ import { buildResourceIndex } from "../resource_index_builder";
|
|||
import {
|
||||
TaggedFarmEvent, TaggedSequence, TaggedRegimen, TaggedImage,
|
||||
TaggedTool, TaggedToolSlotPointer, TaggedUser, TaggedWebcamFeed,
|
||||
TaggedPlantPointer, TaggedGenericPointer
|
||||
TaggedPlantPointer, TaggedGenericPointer, TaggedPeripheral
|
||||
} from "../../resources/tagged_resources";
|
||||
import { ExecutableType } from "../../farm_designer/interfaces";
|
||||
import { fakeResource } from "../fake_resource";
|
||||
|
@ -123,3 +123,11 @@ export function fakeWebcamFeed(): TaggedWebcamFeed {
|
|||
name: "wcf #" + id
|
||||
});
|
||||
}
|
||||
|
||||
export function fakePeripheral(): TaggedPeripheral {
|
||||
return fakeResource("peripherals", {
|
||||
id: idCounter++,
|
||||
label: "Fake Pin",
|
||||
pin: 1
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import { Everything } from "../../interfaces";
|
||||
import { AuthState } from "../../auth/interfaces";
|
||||
|
||||
export let auth: Everything["auth"] = {
|
||||
export let auth: AuthState = {
|
||||
"token": {
|
||||
"unencoded": {
|
||||
"iat": 1495569084,
|
||||
"jti": "b38915ca-3d7a-4754-8152-d4306b88504c",
|
||||
"iss": "//localhost:3000",
|
||||
"exp": 1499025084,
|
||||
"mqtt": "10.0.0.6",
|
||||
"os_update_server": "https://api.github.com/repos/farmbot/" +
|
||||
"farmbot_os/releases/latest"
|
||||
},
|
||||
|
@ -24,7 +20,6 @@ export let auth: Everything["auth"] = {
|
|||
"XV6SGE624zdr7S7mQ6uj7qpa2LMH4P37R3BIB26G7E8xDcVOGqL5Oiwr9DPajBX3zd" +
|
||||
"hXSbH3k4PyxqvPOLYso-R7kjfpOnfFCMfMZLW8TQtg-yj82zs93RP2DHOOx-jxek69" +
|
||||
"tmgNyP3FJaoWHwHW7bXOEv09p3dhNVTCSVNKD9LZczLpuXV7U4oSmL6KLkbzsM6G0P" +
|
||||
|
||||
"9rrbJ9ASYaOw"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,7 +19,6 @@ jest.mock("../session", () => {
|
|||
|
||||
import { maybeRefreshToken } from "../refresh_token";
|
||||
import { API } from "../api/index";
|
||||
import { Session } from "../session";
|
||||
|
||||
API.setBaseUrl("http://blah.whatever.party");
|
||||
|
||||
|
@ -30,8 +29,6 @@ describe("maybeRefreshToken()", () => {
|
|||
token: {
|
||||
encoded: "---",
|
||||
unencoded: {
|
||||
iat: 123,
|
||||
jti: "111",
|
||||
iss: "---",
|
||||
exp: 456,
|
||||
mqtt: "---",
|
||||
|
@ -39,8 +36,8 @@ describe("maybeRefreshToken()", () => {
|
|||
}
|
||||
}
|
||||
};
|
||||
maybeRefreshToken(t).then(() => {
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
maybeRefreshToken(t).then((result) => {
|
||||
expect(result).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
const mockAuth = (jti = "456"): AuthState => ({
|
||||
const mockAuth = (iss = "987"): AuthState => ({
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: {
|
||||
iat: 123,
|
||||
jti,
|
||||
iss: "---",
|
||||
exp: 456,
|
||||
mqtt: "---",
|
||||
os_update_server: "---"
|
||||
}
|
||||
unencoded: { iss, os_update_server: "---" }
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -34,8 +27,12 @@ describe("maybeRefreshToken()", () => {
|
|||
it("gives you back your token when things fail", (done) => {
|
||||
maybeRefreshToken(mockAuth("111"))
|
||||
.then((nextToken) => {
|
||||
expect(nextToken.token.unencoded.jti).toEqual("000");
|
||||
done();
|
||||
if (nextToken) {
|
||||
expect(nextToken.token.unencoded.iss).toEqual("000");
|
||||
done();
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ jest.mock("../util", () => ({ getParam: () => "STUB_PARAM" }));
|
|||
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
put: () => ({ data: { FAKE_TOKEN: true } })
|
||||
put: jest.fn(() => ({ data: { FAKE_TOKEN: true } }))
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -23,17 +23,25 @@ jest.mock("../api/api", () => ({
|
|||
}
|
||||
}));
|
||||
|
||||
import { fail, FAILURE_PAGE, attempt } from "../verification_support";
|
||||
import {
|
||||
fail,
|
||||
FAILURE_PAGE,
|
||||
attempt
|
||||
} from "../verification_support";
|
||||
import { API } from "../api/api";
|
||||
import { Session } from "../session";
|
||||
import axios from "axios";
|
||||
import { getParam } from "../util";
|
||||
|
||||
describe("fail()", () => {
|
||||
it("writes a failure message", () => {
|
||||
it("writes a failure message - base case", () => {
|
||||
expect(fail).toThrow();
|
||||
expect(document.documentElement.outerHTML).toContain(FAILURE_PAGE);
|
||||
});
|
||||
|
||||
it("writes a failure message - base case", () => {
|
||||
expect(() => fail({ response: { status: 409 } } as any)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("attempt()", () => {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
jest.mock("../../session", () => ({
|
||||
Session: {
|
||||
clear: jest.fn(),
|
||||
getBool: () => true,
|
||||
fetchStoredToken: () => ({})
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("farmbot-toastr", () => ({
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("axios", () => ({
|
||||
default: {
|
||||
interceptors: {
|
||||
response: { use: jest.fn() },
|
||||
request: { use: jest.fn() }
|
||||
},
|
||||
post: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }),
|
||||
get: jest.fn(() => { return Promise.resolve({ data: { foo: "bar" } }); }),
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../api/api", () => ({
|
||||
API: {
|
||||
setBaseUrl: jest.fn(),
|
||||
inferPort: () => 443,
|
||||
current: {
|
||||
tokensPath: "/api/tokenStub",
|
||||
usersPath: "/api/userStub"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
import { Session } from "../../session";
|
||||
import { logout, requestToken, requestRegistration, didLogin } from "../actions";
|
||||
import { Actions } from "../../constants";
|
||||
import { success } from "farmbot-toastr";
|
||||
import { API } from "../../api/api";
|
||||
import axios from "axios";
|
||||
import { AuthState } from "../interfaces";
|
||||
|
||||
describe("logout()", () => {
|
||||
it("displays the toast if you are logged out", () => {
|
||||
const result = logout();
|
||||
expect(result.type).toEqual(Actions.LOGOUT);
|
||||
expect(success).toHaveBeenCalledWith("You have been logged out.");
|
||||
expect(Session.clear).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestToken()", () => {
|
||||
it("requests an auth token over HTTP", async () => {
|
||||
const url = "geocities.com";
|
||||
const email = "foo@bar.com";
|
||||
const password = "password123";
|
||||
const result = await requestToken(email, password, url);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith("/api/tokenStub",
|
||||
{ user: { email, password } });
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.data.foo).toEqual("bar");
|
||||
expect(API.setBaseUrl).toHaveBeenCalledWith(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestRegistration", () => {
|
||||
it("sends registration to the API", async () => {
|
||||
const inputs = {
|
||||
email: "foo@bar.co",
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
name: "Paul"
|
||||
};
|
||||
const resp = await requestRegistration(inputs.name,
|
||||
inputs.email,
|
||||
inputs.password,
|
||||
inputs.password_confirmation);
|
||||
|
||||
expect(resp.data.foo).toEqual("bar");
|
||||
expect(axios.post).toHaveBeenCalledWith("/api/userStub", { user: inputs });
|
||||
});
|
||||
});
|
||||
|
||||
describe("didLogin()", () => {
|
||||
it("bootstraps the user session", () => {
|
||||
const mockToken: AuthState = {
|
||||
token: {
|
||||
encoded: "---",
|
||||
unencoded: { iss: "iss", os_update_server: "os_update_server" }
|
||||
}
|
||||
};
|
||||
const dispatch = jest.fn();
|
||||
const result = didLogin(mockToken, dispatch);
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const { iss } = mockToken.token.unencoded;
|
||||
expect(API.setBaseUrl).toHaveBeenCalledWith(iss);
|
||||
const actions = dispatch.mock.calls.map(x => x && x[0] && x[0].type);
|
||||
expect(actions).toContain(Actions.REPLACE_TOKEN);
|
||||
});
|
||||
});
|
|
@ -70,13 +70,8 @@ export function register(name: string,
|
|||
confirmation: string,
|
||||
url: string): Thunk {
|
||||
return dispatch => {
|
||||
const p = requestRegistration(name,
|
||||
email,
|
||||
password,
|
||||
confirmation,
|
||||
url);
|
||||
return p.then(onLogin(dispatch),
|
||||
onRegistrationErr(dispatch));
|
||||
return requestRegistration(name, email, password, confirmation)
|
||||
.then(onLogin(dispatch), onRegistrationErr(dispatch));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -87,24 +82,17 @@ export function onRegistrationErr(dispatch: Function) {
|
|||
|
||||
/** Build a JSON object in preparation for an HTTP POST
|
||||
* to registration endpoint */
|
||||
function requestRegistration(name: string,
|
||||
export function requestRegistration(name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
confirmation: string,
|
||||
url: string) {
|
||||
const form = {
|
||||
user: {
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: confirmation,
|
||||
name: name
|
||||
}
|
||||
};
|
||||
password_confirmation: string) {
|
||||
|
||||
const form = { user: { email, password, password_confirmation, name } };
|
||||
return axios.post(API.current.usersPath, form);
|
||||
}
|
||||
|
||||
/** Fetch API token if already registered. */
|
||||
function requestToken(email: string,
|
||||
export function requestToken(email: string,
|
||||
password: string,
|
||||
url: string) {
|
||||
const payload = { user: { email: email, password: password } };
|
||||
|
|
|
@ -8,16 +8,10 @@ export interface AuthState {
|
|||
}
|
||||
|
||||
export interface UnencodedToken {
|
||||
/** ISSUED AT */
|
||||
iat: number;
|
||||
/** JSON TOKEN IDENTIFIER - a serial number for the token. */
|
||||
jti: string;
|
||||
/** ISSUER - Where token came from (API URL). */
|
||||
iss: string;
|
||||
/** EXPIRATION DATE */
|
||||
exp: number;
|
||||
/** MQTT server address */
|
||||
mqtt: string;
|
||||
// mqtt: string;
|
||||
/** Where to download RPi software */
|
||||
os_update_server: string;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,15 @@ import { maybeRefreshToken } from "../refresh_token";
|
|||
import { withTimeout } from "../util";
|
||||
import { AuthState } from "../auth/interfaces";
|
||||
|
||||
export const storeToken = (auth: AuthState, dispatch: Function) => () => {
|
||||
dispatch(setToken(auth));
|
||||
didLogin(auth, dispatch);
|
||||
};
|
||||
export const storeToken =
|
||||
(old: AuthState, dispatch: Function) => (_new: AuthState | undefined) => {
|
||||
const t = _new || old;
|
||||
if (!_new) {
|
||||
console.warn("Failed to refresh token. Something is wrong.");
|
||||
}
|
||||
dispatch(setToken(t));
|
||||
didLogin(t, dispatch);
|
||||
};
|
||||
|
||||
/** Amount of time we're willing to wait before concluding that the token is bad
|
||||
* or the API is down. */
|
||||
|
@ -22,8 +27,10 @@ export function ready(): Thunk {
|
|||
return (dispatch, getState) => {
|
||||
const auth = Session.fetchStoredToken() || getState().auth;
|
||||
if (auth) {
|
||||
withTimeout(MAX_TOKEN_WAIT_TIME, maybeRefreshToken(auth))
|
||||
.then(storeToken(auth, dispatch), Session.clear);
|
||||
const ok = storeToken(auth, dispatch);
|
||||
const no = () => ok(undefined);
|
||||
const p = maybeRefreshToken(auth);
|
||||
withTimeout(MAX_TOKEN_WAIT_TIME, p).then(ok, no);
|
||||
} else {
|
||||
Session.clear();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
const mockError = jest.fn();
|
||||
jest.mock("farmbot-toastr", () => ({
|
||||
error: mockError
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Peripherals } from "../index";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
import { PeripheralsProps } from "../../../devices/interfaces";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { fakePeripheral } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<Peripherals />", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): PeripheralsProps {
|
||||
return {
|
||||
bot,
|
||||
peripherals: [fakePeripheral()],
|
||||
dispatch: jest.fn(),
|
||||
resources: buildResourceIndex([]),
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<Peripherals {...fakeProps() } />);
|
||||
["Peripherals", "Edit", "Save", "Fake Pin", "1"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
const saveButton = wrapper.find("button").at(1);
|
||||
expect(saveButton.text()).toContain("Save");
|
||||
expect(saveButton.props().hidden).toBeTruthy();
|
||||
});
|
||||
|
||||
it("isEditing", () => {
|
||||
const wrapper = mount(<Peripherals {...fakeProps() } />);
|
||||
expect(wrapper.state().isEditing).toBeFalsy();
|
||||
const edit = wrapper.find("button").at(0);
|
||||
expect(edit.text()).toEqual("Edit");
|
||||
edit.simulate("click");
|
||||
expect(wrapper.state().isEditing).toBeTruthy();
|
||||
});
|
||||
|
||||
function attemptSave(num: number, error: string) {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].body.pin = num;
|
||||
const wrapper = mount(<Peripherals {...p } />);
|
||||
const save = wrapper.find("button").at(1);
|
||||
expect(save.text()).toContain("Save");
|
||||
save.simulate("click");
|
||||
expect(mockError).toHaveBeenLastCalledWith(error);
|
||||
}
|
||||
|
||||
it("save attempt: pin number too small", () => {
|
||||
attemptSave(0, "Pin numbers are required and must be positive and unique.");
|
||||
});
|
||||
|
||||
it("save attempt: pin number too large", () => {
|
||||
attemptSave(9999, "Pin numbers must be less than 1000.");
|
||||
});
|
||||
|
||||
it("saves", () => {
|
||||
const p = fakeProps();
|
||||
p.peripherals[0].body.pin = 1;
|
||||
const wrapper = mount(<Peripherals {...p } />);
|
||||
const save = wrapper.find("button").at(1);
|
||||
expect(save.text()).toContain("Save");
|
||||
save.simulate("click");
|
||||
expect(p.dispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -154,7 +154,7 @@ export let fetchReleases =
|
|||
const version = resp.data.tag_name;
|
||||
const versionWithoutV = version.toLowerCase().replace("v", "");
|
||||
dispatch({
|
||||
type: "FETCH_OS_UPDATE_INFO_OK",
|
||||
type: Actions.FETCH_OS_UPDATE_INFO_OK,
|
||||
payload: versionWithoutV
|
||||
});
|
||||
})
|
||||
|
|
|
@ -15,14 +15,8 @@ describe("<FarmbotOsSettings/>", () => {
|
|||
auth={fakeState().auth as AuthState} />);
|
||||
expect(osSettings.find("input").length).toBe(1);
|
||||
expect(osSettings.find("button").length).toBe(9);
|
||||
expect(osSettings.text()).toContain("NAME");
|
||||
expect(osSettings.text()).toContain("TIME ZONE");
|
||||
expect(osSettings.text()).toContain("LAST SEEN");
|
||||
expect(osSettings.text()).toContain("FARMBOT OS");
|
||||
expect(osSettings.text()).toContain("RESTART FARMBOT");
|
||||
expect(osSettings.text()).toContain("SHUTDOWN FARMBOT");
|
||||
expect(osSettings.text()).toContain("FACTORY RESET");
|
||||
expect(osSettings.text()).toContain("CAMERA");
|
||||
expect(osSettings.text()).toContain("FIRMWARE");
|
||||
["NAME", "TIME ZONE", "LAST SEEN", "FARMBOT OS", "RESTART FARMBOT",
|
||||
"SHUTDOWN FARMBOT", "FACTORY RESET", "CAMERA", "FIRMWARE"].map(string =>
|
||||
expect(osSettings.text()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { mount } from "enzyme";
|
|||
import { HardwareSettings } from "../hardware_settings";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { ControlPanelState } from "../../interfaces";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<HardwareSettings />", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -23,9 +24,8 @@ describe("<HardwareSettings />", () => {
|
|||
controlPanelState={panelState()}
|
||||
dispatch={jest.fn()}
|
||||
bot={fakeState().bot} />);
|
||||
expect(wrapper.text().toLowerCase()).toContain("expand all");
|
||||
expect(wrapper.text().toLowerCase()).toContain("x axis");
|
||||
expect(wrapper.text().toLowerCase()).toContain("motors");
|
||||
["expand all", "x axis", "motors"].map(string =>
|
||||
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||
});
|
||||
|
||||
function checkDispatch(
|
||||
|
@ -46,14 +46,17 @@ describe("<HardwareSettings />", () => {
|
|||
}
|
||||
|
||||
it("expands all", () => {
|
||||
checkDispatch("button", 1, "expand all", "BULK_TOGGLE_CONTROL_PANEL", true);
|
||||
checkDispatch("button", 1, "expand all",
|
||||
Actions.BULK_TOGGLE_CONTROL_PANEL, true);
|
||||
});
|
||||
|
||||
it("collapses all", () => {
|
||||
checkDispatch("button", 2, "collapse all", "BULK_TOGGLE_CONTROL_PANEL", false);
|
||||
checkDispatch("button", 2, "collapse all",
|
||||
Actions.BULK_TOGGLE_CONTROL_PANEL, false);
|
||||
});
|
||||
|
||||
it("toggles motor category", () => {
|
||||
checkDispatch("h4", 1, "motors", "TOGGLE_CONTROL_PANEL_OPTION", "motors");
|
||||
checkDispatch("h4", 1, "motors",
|
||||
Actions.TOGGLE_CONTROL_PANEL_OPTION, "motors");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../../history", () => ({
|
||||
history: {
|
||||
push: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { AddFarmEvent } from "../add_farm_event";
|
||||
import { AddEditFarmEventProps } from "../../interfaces";
|
||||
import { fakeFarmEvent, fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<AddFarmEvent />", () => {
|
||||
function fakeProps(): AddEditFarmEventProps {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.id = 1;
|
||||
const farmEvent = fakeFarmEvent("Sequence", 1);
|
||||
farmEvent.uuid = "farm_events";
|
||||
return {
|
||||
deviceTimezone: "",
|
||||
dispatch: jest.fn(),
|
||||
regimensById: {},
|
||||
sequencesById: { "1": sequence },
|
||||
farmEventsById: { "1": farmEvent },
|
||||
executableOptions: [],
|
||||
repeatOptions: [],
|
||||
formatDate: jest.fn(),
|
||||
formatTime: jest.fn(),
|
||||
handleTime: jest.fn(),
|
||||
farmEvents: [farmEvent],
|
||||
getFarmEvent: () => farmEvent,
|
||||
findExecutable: () => sequence
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<AddFarmEvent {...fakeProps() } />);
|
||||
wrapper.setState({ uuid: "farm_events" });
|
||||
["Add Farm Event", "Sequence or Regimen", "fake"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("redirects", () => {
|
||||
const wrapper = mount(<AddFarmEvent {...fakeProps() } />);
|
||||
expect(wrapper.text()).toContain("Loading");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("../../../history", () => ({
|
||||
history: {
|
||||
push: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { EditFarmEvent } from "../edit_farm_event";
|
||||
import { AddEditFarmEventProps } from "../../interfaces";
|
||||
import { fakeFarmEvent, fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<EditFarmEvent />", () => {
|
||||
function fakeProps(): AddEditFarmEventProps {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.id = 1;
|
||||
return {
|
||||
deviceTimezone: "",
|
||||
dispatch: jest.fn(),
|
||||
regimensById: {},
|
||||
sequencesById: { "1": sequence },
|
||||
farmEventsById: { "1": fakeFarmEvent("Sequence", 1) },
|
||||
executableOptions: [],
|
||||
repeatOptions: [],
|
||||
formatDate: jest.fn(),
|
||||
formatTime: jest.fn(),
|
||||
handleTime: jest.fn(),
|
||||
farmEvents: [],
|
||||
getFarmEvent: () => fakeFarmEvent("Sequence", 1),
|
||||
findExecutable: () => sequence
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<EditFarmEvent {...fakeProps() } />);
|
||||
["Edit Farm Event", "Sequence or Regimen", "fake"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("redirects", () => {
|
||||
const p = fakeProps();
|
||||
p.getFarmEvent = jest.fn();
|
||||
const wrapper = mount(<EditFarmEvent {...p } />);
|
||||
expect(wrapper.text()).toContain("Loading");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import { mapStateToProps } from "../map_state_to_props";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import {
|
||||
fakeSequence,
|
||||
fakeRegimen,
|
||||
fakeFarmEvent
|
||||
} from "../../../__test_support__/fake_state/resources";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import * as moment from "moment";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
function testState(time: number) {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.id = 1;
|
||||
sequence.body.body = [{ kind: "take_photo", args: {} }];
|
||||
const regimen = fakeRegimen();
|
||||
regimen.body.id = 1;
|
||||
regimen.body.regimen_items = [{
|
||||
sequence_id: 1,
|
||||
time_offset: moment(time).add(10, "minutes")
|
||||
.diff(moment(time).clone().startOf("day"), "milliseconds")
|
||||
}];
|
||||
|
||||
const getFutureTime = (t: number, value: number, label: string) =>
|
||||
// tslint:disable-next-line:no-any
|
||||
moment(t).add(value as any, label).toISOString();
|
||||
|
||||
const sequenceFarmEvent = fakeFarmEvent("Sequence", 1);
|
||||
sequenceFarmEvent.body.id = 1;
|
||||
const plusOneDay = moment(getFutureTime(time, 1, "day")).valueOf();
|
||||
sequenceFarmEvent.body.start_time = getFutureTime(plusOneDay, 2, "minutes");
|
||||
sequenceFarmEvent.body.end_time = getFutureTime(plusOneDay, 3, "minutes");
|
||||
|
||||
const regimenFarmEvent = fakeFarmEvent("Regimen", 1);
|
||||
regimenFarmEvent.body.id = 2;
|
||||
const plusTwoDays = moment(getFutureTime(time, 2, "days")).valueOf();
|
||||
regimenFarmEvent.body.start_time = getFutureTime(plusTwoDays, 1, "minute");
|
||||
regimenFarmEvent.body.end_time = getFutureTime(plusTwoDays, 2, "minutes");
|
||||
|
||||
const fakeResources = [
|
||||
sequence,
|
||||
regimen,
|
||||
sequenceFarmEvent,
|
||||
regimenFarmEvent
|
||||
];
|
||||
|
||||
const state = fakeState();
|
||||
state.resources = buildResourceIndex(fakeResources);
|
||||
return state;
|
||||
}
|
||||
|
||||
it("returns calendar rows", () => {
|
||||
const testTime = moment().startOf("hour").valueOf();
|
||||
const { calendarRows, push } = mapStateToProps(testState(testTime));
|
||||
|
||||
// Day 1: Sequence Farm Event
|
||||
const day1 = calendarRows[0];
|
||||
const day1Time = moment(testTime).add(1, "day");
|
||||
expect(day1.year).toEqual(day1Time.year() - 2000);
|
||||
expect(day1.month).toEqual(day1Time.format("MMM"));
|
||||
expect(day1.day).toEqual(day1Time.date());
|
||||
const day1ItemTime = day1Time.add(2, "minutes");
|
||||
expect(day1.sortKey).toEqual(day1ItemTime.unix());
|
||||
expect(day1.items).toEqual([{
|
||||
executableId: 1,
|
||||
heading: "fake",
|
||||
id: 1,
|
||||
mmddyy: day1ItemTime.format("MMDDYY"),
|
||||
sortKey: day1ItemTime.unix(),
|
||||
timeStr: day1ItemTime.format("hh:mma")
|
||||
}]);
|
||||
|
||||
// Day 2: Regimen Farm Event
|
||||
const day2 = calendarRows[1];
|
||||
const day2Time = moment(testTime).add(2, "days");
|
||||
expect(day2.year).toEqual(day2Time.year() - 2000);
|
||||
expect(day2.month).toEqual(day2Time.format("MMM"));
|
||||
expect(day2.day).toEqual(day2Time.date());
|
||||
|
||||
// Regimen
|
||||
const regimenStartTime = day2Time.clone().add(1, "minutes");
|
||||
expect(day2.sortKey).toEqual(regimenStartTime.unix());
|
||||
expect(day2.items[0]).toEqual({
|
||||
executableId: 1,
|
||||
heading: "Foo",
|
||||
id: 2,
|
||||
mmddyy: regimenStartTime.format("MMDDYY"),
|
||||
sortKey: regimenStartTime.unix(),
|
||||
subheading: "",
|
||||
timeStr: regimenStartTime.format("hh:mma")
|
||||
});
|
||||
|
||||
// Regimen Item
|
||||
const regimenItemTime = day2Time.clone().add(10, "minutes");
|
||||
expect(day2.items[1]).toEqual({
|
||||
executableId: 1,
|
||||
heading: "Foo",
|
||||
id: 2,
|
||||
mmddyy: regimenItemTime.format("MMDDYY"),
|
||||
sortKey: regimenItemTime.unix(),
|
||||
subheading: "fake",
|
||||
timeStr: regimenItemTime.format("hh:mma")
|
||||
});
|
||||
|
||||
expect(push).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -139,9 +139,8 @@ describe("SpreadOverlapHelper functions", () => {
|
|||
it("overlapText()", () => {
|
||||
const spreadData = { active: 100, inactive: 200 };
|
||||
const svgText = shallow(overlapText(100, 100, 150, spreadData));
|
||||
expect(svgText.text()).toContain("Active: 80%");
|
||||
expect(svgText.text()).toContain("Inactive: 40%");
|
||||
expect(svgText.text()).toContain("orange");
|
||||
["Active: 80%", "Inactive: 40%", "orange"].map(string =>
|
||||
expect(svgText.text()).toContain(string));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -21,10 +21,11 @@ describe("<DragHelperLayer/>", () => {
|
|||
it("shows drag helpers", () => {
|
||||
const p = fakeProps();
|
||||
const wrapper = shallow(<DragHelperLayer {...p } />);
|
||||
expect(wrapper.html()).toContain("drag-helpers");
|
||||
expect(wrapper.html()).toContain("coordinates-tooltip");
|
||||
expect(wrapper.html()).toContain("long-crosshair");
|
||||
expect(wrapper.html()).toContain("short-crosshair");
|
||||
["drag-helpers",
|
||||
"coordinates-tooltip",
|
||||
"long-crosshair",
|
||||
"short-crosshair"].map(string =>
|
||||
expect(wrapper.html()).toContain(string));
|
||||
});
|
||||
|
||||
it("doesn't show drag helpers", () => {
|
||||
|
|
|
@ -35,13 +35,15 @@ describe("<PlantLayer/>", () => {
|
|||
const wrapper = shallow(<PlantLayer {...p } />);
|
||||
const layer = wrapper.find("#plant-layer");
|
||||
expect(layer.find(".plant-link-wrapper").length).toEqual(1);
|
||||
expect(layer.html()).toContain("soil-cloud");
|
||||
expect(layer.html()).toContain("plant-icon");
|
||||
expect(layer.html()).toContain("image visibility=\"visible\"");
|
||||
expect(layer.html()).toContain("/app-resources/img/generic-plant.svg");
|
||||
expect(layer.html()).toContain("height=\"50\" width=\"50\" x=\"75\" y=\"175\"");
|
||||
expect(layer.html()).toContain("drag-helpers");
|
||||
expect(layer.html()).toContain("plant-icon");
|
||||
["soil-cloud",
|
||||
"plant-icon",
|
||||
"image visibility=\"visible\"",
|
||||
"/app-resources/img/generic-plant.svg",
|
||||
"height=\"50\" width=\"50\" x=\"75\" y=\"175\"",
|
||||
"drag-helpers",
|
||||
"plant-icon"
|
||||
].map(string =>
|
||||
expect(layer.html()).toContain(string));
|
||||
});
|
||||
|
||||
it("toggles visibility off", () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
|||
import { PlantInventoryItem } from "../plant_inventory_item";
|
||||
import { shallow } from "enzyme";
|
||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<PlantInventoryItem />", () => {
|
||||
it("renders", () => {
|
||||
|
@ -24,7 +25,7 @@ describe("<PlantInventoryItem />", () => {
|
|||
icon: "",
|
||||
plantUUID: "points.1.17"
|
||||
},
|
||||
type: "TOGGLE_HOVERED_PLANT"
|
||||
type: Actions.TOGGLE_HOVERED_PLANT
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -40,7 +41,7 @@ describe("<PlantInventoryItem />", () => {
|
|||
icon: "",
|
||||
plantUUID: undefined
|
||||
},
|
||||
type: "TOGGLE_HOVERED_PLANT"
|
||||
type: Actions.TOGGLE_HOVERED_PLANT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,8 +13,12 @@ describe("<PlantInventory />", () => {
|
|||
<Plants
|
||||
plants={[fakePlant()]}
|
||||
dispatch={jest.fn()} />);
|
||||
expect(wrapper.text()).toContain("DesignerPlantsFarm Events");
|
||||
expect(wrapper.text()).toContain("Strawberry Plant 11 days old");
|
||||
["Designer",
|
||||
"Plants",
|
||||
"Farm Events",
|
||||
"Strawberry Plant",
|
||||
"11 days old"
|
||||
].map(string => expect(wrapper.text()).toContain(string));
|
||||
expect(wrapper.find("input").props().placeholder)
|
||||
.toEqual("Search your plants...");
|
||||
});
|
||||
|
|
|
@ -38,9 +38,8 @@ describe("<SelectPlants />", () => {
|
|||
const p = fakeProps();
|
||||
p.selected = ["plant.1", "plant.2"];
|
||||
const wrapper = mount(<SelectPlants {...p} />);
|
||||
expect(wrapper.text()).toContain("Strawberry");
|
||||
expect(wrapper.text()).toContain("Blueberry");
|
||||
expect(wrapper.text()).toContain("Delete");
|
||||
["Strawberry", "Blueberry", "Delete"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("displays no selected plants: selection empty", () => {
|
||||
|
|
|
@ -49,9 +49,8 @@ describe("<FarmwareForms/>", () => {
|
|||
const wrapper = mount(<FarmwareForms
|
||||
farmwares={fakeFarmwares()}
|
||||
user_env={{}} />);
|
||||
expect(wrapper.text()).toContain("My Farmware");
|
||||
expect(wrapper.text()).toContain("version: 0.0.0");
|
||||
expect(wrapper.text()).toContain("Does things.");
|
||||
["My Farmware", "version: 0.0.0", "Does things."].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
expect(wrapper.find("label").last().text()).toContain("Config 1");
|
||||
expect(wrapper.find("input").props().value).toEqual("4");
|
||||
});
|
||||
|
|
|
@ -25,9 +25,11 @@ describe("<FarmwarePage />", () => {
|
|||
images: []
|
||||
};
|
||||
const wrapper = mount(<FarmwarePage {...props} />);
|
||||
expect(wrapper.text()).toContain("Take Photo");
|
||||
expect(wrapper.text()).toContain("Farmware");
|
||||
expect(wrapper.text()).toContain("Camera Calibration");
|
||||
expect(wrapper.text()).toContain("Weed Detector");
|
||||
["Take Photo",
|
||||
"Farmware",
|
||||
"Camera Calibration",
|
||||
"Weed Detector"
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,12 +21,14 @@ describe("<CameraCalibration/>", () => {
|
|||
V_HI: 9
|
||||
};
|
||||
const wrapper = mount(<CameraCalibration {...props} />);
|
||||
expect(wrapper.text()).toContain("Camera Calibration");
|
||||
expect(wrapper.text()).toContain("Color Range");
|
||||
expect(wrapper.text()).toContain("HUE017947");
|
||||
expect(wrapper.text()).toContain("SATURATION025558");
|
||||
expect(wrapper.text()).toContain("VALUE025569");
|
||||
expect(wrapper.text()).toContain("Processing Parameters");
|
||||
expect(wrapper.text()).toContain("Scan image");
|
||||
["Camera Calibration",
|
||||
"Color Range",
|
||||
"HUE017947",
|
||||
"SATURATION025558",
|
||||
"VALUE025569",
|
||||
"Processing Parameters",
|
||||
"Scan image"
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,12 +19,14 @@ describe("<WeedDetector />", () => {
|
|||
images: []
|
||||
};
|
||||
const wrapper = mount(<WeedDetector {...props} />);
|
||||
expect(wrapper.text()).toContain("Weed Detector");
|
||||
expect(wrapper.text()).toContain("Color Range");
|
||||
expect(wrapper.text()).toContain("HUE01793090");
|
||||
expect(wrapper.text()).toContain("SATURATION025550255");
|
||||
expect(wrapper.text()).toContain("VALUE025550255");
|
||||
expect(wrapper.text()).toContain("Processing Parameters");
|
||||
expect(wrapper.text()).toContain("Scan image");
|
||||
["Weed Detector",
|
||||
"Color Range",
|
||||
"HUE01793090",
|
||||
"SATURATION025550255",
|
||||
"VALUE025550255",
|
||||
"Processing Parameters",
|
||||
"Scan image"
|
||||
].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
jest.mock("react-dom", () => ({ render: jest.fn() }));
|
||||
|
||||
import { onInit } from "../on_init";
|
||||
import { render } from "react-dom";
|
||||
|
||||
describe("onInit()", () => {
|
||||
it("Attaches to a DOM element", async (done) => {
|
||||
await onInit({}, jest.fn());
|
||||
expect({}).toBeTruthy();
|
||||
expect(render).toHaveBeenCalled();
|
||||
const [calls] = (render as jest.Mock<{}>).mock.calls;
|
||||
expect(calls[0].type.name).toBe("PasswordReset");
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -1,23 +1,6 @@
|
|||
import * as React from "react";
|
||||
import "../css/_index.scss";
|
||||
import { detectLanguage } from "../i18n";
|
||||
import * as I18n from "i18next";
|
||||
import { onInit } from "./on_init";
|
||||
|
||||
detectLanguage().then(async (config) => {
|
||||
const i18next = await import("i18next");
|
||||
const { render } = await import("react-dom");
|
||||
const { PasswordReset } = await import("./password_reset");
|
||||
i18next.init(config, (err, t) => {
|
||||
const node = document.createElement("DIV");
|
||||
node.id = "root";
|
||||
document.body.appendChild(node);
|
||||
|
||||
const reactElem = React.createElement(PasswordReset, {});
|
||||
const domElem = document.getElementById("root");
|
||||
|
||||
if (domElem) {
|
||||
render(reactElem, domElem);
|
||||
} else {
|
||||
throw new Error(t("Add a div with id `root` to the page first."));
|
||||
}
|
||||
});
|
||||
});
|
||||
detectLanguage().then(async config => I18n.init(config, onInit));
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import * as I18n from "i18next";
|
||||
|
||||
export const MISSING_DIV = "Add a div with id `root` to the page first.";
|
||||
|
||||
export const onInit: I18n.Callback = async (err, t) => {
|
||||
const React = await import("react");
|
||||
const { render } = await import("react-dom");
|
||||
const { PasswordReset } = await import("./password_reset");
|
||||
const { bail } = await import("../util");
|
||||
const node = document.createElement("DIV");
|
||||
node.id = "root";
|
||||
document.body.appendChild(node);
|
||||
|
||||
const reactElem = React.createElement(PasswordReset, {});
|
||||
const domElem = document.getElementById("root");
|
||||
|
||||
return (domElem) ? render(reactElem, domElem) : bail(MISSING_DIV);
|
||||
};
|
|
@ -3,7 +3,6 @@ import { API } from "./api/index";
|
|||
import { AuthState } from "./auth/interfaces";
|
||||
import { HttpData } from "./util";
|
||||
import { setToken } from "./auth/actions";
|
||||
import { Session } from "./session";
|
||||
|
||||
type Resp = HttpData<AuthState>;
|
||||
|
||||
|
@ -15,8 +14,11 @@ const ok = (x: Resp) => {
|
|||
|
||||
/** Grab a new token from the API (won't extend token's exp. date).
|
||||
* Redirect to home page on failure. */
|
||||
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
|
||||
API.setBaseUrl(old.token.unencoded.iss);
|
||||
setToken(old); // Precaution: The Axios interceptors might not be set yet.
|
||||
return axios.get(API.current.tokensPath).then(ok, Session.clear);
|
||||
};
|
||||
export let maybeRefreshToken
|
||||
= (old: AuthState): Promise<AuthState | undefined> => {
|
||||
API.setBaseUrl(old.token.unencoded.iss);
|
||||
setToken(old); // Precaution: The Axios interceptors might not be set yet.
|
||||
return axios
|
||||
.get(API.current.tokensPath)
|
||||
.then(ok, () => Promise.resolve(undefined));
|
||||
};
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { editRegimen, saveRegimen, deleteRegimen, selectRegimen } from "../actions";
|
||||
import { fakeRegimen } from "../../__test_support__/fake_state/resources";
|
||||
import { Actions } from "../../constants";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("editRegimen()", () => {
|
||||
it("doesn't call edit", () => {
|
||||
const dispatch = jest.fn();
|
||||
editRegimen(undefined, {})(dispatch);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls edit", () => {
|
||||
const dispatch = jest.fn();
|
||||
const regimen = fakeRegimen();
|
||||
regimen.uuid = "regimens";
|
||||
editRegimen(regimen, {})(dispatch);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
update: {},
|
||||
uuid: "regimens"
|
||||
},
|
||||
type: Actions.EDIT_RESOURCE
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveRegimen()", () => {
|
||||
it("calls save", () => {
|
||||
const dispatch = jest.fn();
|
||||
const state = fakeState();
|
||||
state.resources.index = buildResourceIndex([fakeRegimen()]).index;
|
||||
const getState = () => state;
|
||||
saveRegimen(state.resources.index.all[0])(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
body: { color: "red", name: "Foo", regimen_items: [] },
|
||||
kind: "regimens",
|
||||
uuid: state.resources.index.all[0]
|
||||
},
|
||||
type: Actions.SAVE_RESOURCE_START
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteRegimen()", () => {
|
||||
it("doesn't delete regimen", () => {
|
||||
const dispatch = jest.fn();
|
||||
const state = fakeState();
|
||||
state.resources.index = buildResourceIndex([fakeRegimen()]).index;
|
||||
const getState = () => state;
|
||||
deleteRegimen(state.resources.index.all[0])(dispatch, getState);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls destroy", () => {
|
||||
const dispatch = jest.fn();
|
||||
const state = fakeState();
|
||||
state.resources.index = buildResourceIndex([fakeRegimen()]).index;
|
||||
const getState = () => state;
|
||||
// tslint:disable-next-line:no-any
|
||||
(global as any).confirm = () => true;
|
||||
deleteRegimen(state.resources.index.all[0])(dispatch, getState);
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
body: { color: "red", name: "Foo", regimen_items: [] },
|
||||
kind: "regimens",
|
||||
uuid: state.resources.index.all[0]
|
||||
},
|
||||
type: Actions.DESTROY_RESOURCE_OK
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectRegimen()", () => {
|
||||
it("selects regimen", () => {
|
||||
const regimen = fakeRegimen();
|
||||
regimen.uuid = "regimens";
|
||||
const action = selectRegimen(regimen);
|
||||
expect(action).toEqual({
|
||||
payload: {
|
||||
body: { color: "red", name: "Foo", regimen_items: [] },
|
||||
kind: "regimens",
|
||||
uuid: "regimens"
|
||||
},
|
||||
type: Actions.SELECT_REGIMEN
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Regimens } from "../index";
|
||||
import { Props } from "../interfaces";
|
||||
import { bot } from "../../__test_support__/fake_state/bot";
|
||||
import { auth } from "../../__test_support__/fake_state/token";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("<Regimens />", () => {
|
||||
it("renders", () => {
|
||||
const fakeProps: Props = {
|
||||
dispatch: jest.fn(),
|
||||
sequences: [],
|
||||
resources: buildResourceIndex([]).index,
|
||||
auth,
|
||||
current: undefined,
|
||||
regimens: [],
|
||||
selectedSequence: undefined,
|
||||
dailyOffsetMs: 1000,
|
||||
weeks: [],
|
||||
bot,
|
||||
calendar: []
|
||||
};
|
||||
const wrapper = mount(<Regimens {...fakeProps } />);
|
||||
["Regimens", "Regimen Editor", "Scheduler"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
|
@ -84,3 +84,42 @@ describe("INIT_RESOURCE", () => {
|
|||
expect(nextState.currentRegimen).toBe(action.payload.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SELECT_REGIMEN", () => {
|
||||
it("sets currentRegimen", () => {
|
||||
const state = defensiveClone(STATE);
|
||||
state.currentRegimen = undefined;
|
||||
const action = {
|
||||
type: Actions.SELECT_REGIMEN,
|
||||
payload: { uuid: "regimens.4.56", kind: "regimens" }
|
||||
};
|
||||
const nextState = regimensReducer(STATE, action);
|
||||
expect(nextState.currentRegimen).toBe(action.payload.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_SEQUENCE", () => {
|
||||
it("sets selectedSequenceUUID", () => {
|
||||
const state = defensiveClone(STATE);
|
||||
state.selectedSequenceUUID = undefined;
|
||||
const action = {
|
||||
type: Actions.SET_SEQUENCE,
|
||||
payload: "sequence"
|
||||
};
|
||||
const nextState = regimensReducer(STATE, action);
|
||||
expect(nextState.selectedSequenceUUID).toBe(action.payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SET_TIME_OFFSET", () => {
|
||||
it("sets dailyOffsetMs", () => {
|
||||
const state = defensiveClone(STATE);
|
||||
state.dailyOffsetMs = NaN;
|
||||
const action = {
|
||||
type: Actions.SET_TIME_OFFSET,
|
||||
payload: 100
|
||||
};
|
||||
const nextState = regimensReducer(STATE, action);
|
||||
expect(nextState.dailyOffsetMs).toBe(action.payload);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { regimenSerializer } from "../serializers";
|
||||
import { fakeRegimen } from "../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("regimenSerializer()", () => {
|
||||
it("returns formatted regimen", () => {
|
||||
const regimen = fakeRegimen().body;
|
||||
regimen.name = "Fake Regimen";
|
||||
regimen.color = "blue";
|
||||
regimen.regimen_items = [];
|
||||
const ret = regimenSerializer(regimen);
|
||||
expect(ret).toEqual({
|
||||
color: "blue",
|
||||
name: "Fake Regimen",
|
||||
regimen_items: []
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { mapStateToProps } from "../state_to_props";
|
||||
import { fakeState } from "../../__test_support__/fake_state";
|
||||
import { TaggedResource } from "../../resources/tagged_resources";
|
||||
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
|
||||
|
||||
describe("mapStateToProps()", () => {
|
||||
it("returns props: no regimen selected", () => {
|
||||
const props = mapStateToProps(fakeState());
|
||||
expect(props.current).toEqual(undefined);
|
||||
expect(props.calendar).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns props: active regimen", () => {
|
||||
const state = fakeState();
|
||||
const fakeResources: TaggedResource[] = [
|
||||
{
|
||||
"specialStatus": undefined,
|
||||
"kind": "regimens",
|
||||
"body": {
|
||||
"id": 1,
|
||||
"name": "Test Regimen",
|
||||
"color": "gray",
|
||||
"regimen_items": [
|
||||
{
|
||||
"id": 1,
|
||||
"regimen_id": 1,
|
||||
"sequence_id": 1,
|
||||
"time_offset": 1000
|
||||
}
|
||||
]
|
||||
},
|
||||
"uuid": "N/A"
|
||||
},
|
||||
{
|
||||
"kind": "sequences",
|
||||
"specialStatus": undefined,
|
||||
"body": {
|
||||
"id": 1,
|
||||
"name": "Test Sequence",
|
||||
"color": "gray",
|
||||
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
|
||||
"args": {
|
||||
"version": 4
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
"uuid": "N/A"
|
||||
}
|
||||
];
|
||||
state.resources.index = buildResourceIndex(fakeResources).index;
|
||||
const regimenUuid = state.resources.index.all[0];
|
||||
state.resources.consumers.regimens.currentRegimen = regimenUuid;
|
||||
const props = mapStateToProps(state);
|
||||
props.current ? expect(props.current.uuid).toEqual(regimenUuid) : fail;
|
||||
expect(props.calendar[0].items[0].item.time_offset).toEqual(1000);
|
||||
});
|
||||
});
|
|
@ -1,16 +1,155 @@
|
|||
const mockErr = jest.fn();
|
||||
jest.mock("i18next", () => ({ t: (i: string) => i }));
|
||||
jest.mock("farmbot-toastr", () => ({ error: mockErr }));
|
||||
jest.mock("farmbot-toastr", () => ({ error: mockErr, warning: mockErr }));
|
||||
|
||||
import { commitBulkEditor } from "../actions";
|
||||
import { commitBulkEditor, setTimeOffset, toggleDay, setSequence } from "../actions";
|
||||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { TaggedResource } from "../../../resources/tagged_resources";
|
||||
import { Actions } from "../../../constants";
|
||||
import { Everything } from "../../../interfaces";
|
||||
import { ToggleDayParams } from "../interfaces";
|
||||
|
||||
describe("commitBulkEditor()", () => {
|
||||
it("does nothing if no regimen is selected", () => {
|
||||
const getState = () => fakeState();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function newFakeState() {
|
||||
const state = fakeState();
|
||||
const fakeResources: TaggedResource[] = [
|
||||
{
|
||||
"specialStatus": undefined,
|
||||
"kind": "regimens",
|
||||
"body": {
|
||||
"id": 1,
|
||||
"name": "Test Regimen",
|
||||
"color": "gray",
|
||||
"regimen_items": [
|
||||
{
|
||||
"id": 1,
|
||||
"regimen_id": 1,
|
||||
"sequence_id": 1,
|
||||
"time_offset": 1000
|
||||
}
|
||||
]
|
||||
},
|
||||
"uuid": "N/A"
|
||||
},
|
||||
{
|
||||
"kind": "sequences",
|
||||
"specialStatus": undefined,
|
||||
"body": {
|
||||
"id": 1,
|
||||
"name": "Test Sequence",
|
||||
"color": "gray",
|
||||
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
|
||||
"args": {
|
||||
"version": 4
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
"uuid": "N/A"
|
||||
}
|
||||
];
|
||||
state.resources.index = buildResourceIndex(fakeResources).index;
|
||||
const regimenUuid = state.resources.index.all[0];
|
||||
const sequenceUuid = state.resources.index.all[1];
|
||||
|
||||
state.resources.consumers.regimens.currentRegimen = regimenUuid;
|
||||
state.resources.consumers.regimens.selectedSequenceUUID = sequenceUuid;
|
||||
state.resources.consumers.regimens.dailyOffsetMs = 2000;
|
||||
state.resources.consumers.regimens.weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
return state;
|
||||
}
|
||||
|
||||
function returnsError(state: Everything, message: string) {
|
||||
const getState = () => state;
|
||||
const dispatch = jest.fn();
|
||||
commitBulkEditor()(dispatch, getState);
|
||||
expect(dispatch.mock.calls.length).toEqual(0);
|
||||
expect(mockErr.mock.calls.length).toEqual(1);
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
expect(mockErr).toBeCalledWith(message);
|
||||
}
|
||||
|
||||
it("does nothing if no regimen is selected", () => {
|
||||
const state = newFakeState();
|
||||
state.resources.consumers.regimens.currentRegimen = undefined;
|
||||
returnsError(state, "Select a regimen first or create one.");
|
||||
});
|
||||
|
||||
it("does nothing if no sequence is selected", () => {
|
||||
const state = newFakeState();
|
||||
state.resources.consumers.regimens.selectedSequenceUUID = undefined;
|
||||
returnsError(state, "Select a sequence from the dropdown first.");
|
||||
});
|
||||
|
||||
it("does nothing if no days are selected", () => {
|
||||
const state = newFakeState();
|
||||
state.resources.consumers.regimens.weeks[0].days.day1 = false;
|
||||
returnsError(state, "No day(s) selected.");
|
||||
});
|
||||
|
||||
it("does nothing if no weeks", () => {
|
||||
const state = newFakeState();
|
||||
state.resources.consumers.regimens.weeks = [];
|
||||
returnsError(state, "No day(s) selected.");
|
||||
});
|
||||
|
||||
it("adds items", () => {
|
||||
const state = newFakeState();
|
||||
const getState = () => state;
|
||||
const dispatch = jest.fn();
|
||||
commitBulkEditor()(dispatch, getState);
|
||||
const argsList = dispatch.mock.calls[0][0];
|
||||
expect(argsList.type).toEqual(Actions.OVERWRITE_RESOURCE);
|
||||
expect(argsList.payload.update.regimen_items[1])
|
||||
.toEqual({ sequence_id: 1, time_offset: 2000 });
|
||||
expect(mockErr).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTimeOffset()", () => {
|
||||
it("returns action", () => {
|
||||
const action = setTimeOffset(100);
|
||||
expect(action).toEqual({ payload: 100, type: Actions.SET_TIME_OFFSET });
|
||||
});
|
||||
|
||||
it("throws error for NaN", () => {
|
||||
expect(() => setTimeOffset(NaN))
|
||||
.toThrowError("Bad time input on regimen page: null");
|
||||
expect(mockErr).toBeCalledWith(
|
||||
"Time is not properly formatted.", "Bad Input");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggleDay()", () => {
|
||||
it("returns action", () => {
|
||||
const params: ToggleDayParams = { week: 0, day: 0 };
|
||||
const action = toggleDay(params);
|
||||
expect(action).toEqual({
|
||||
payload: { day: 0, week: 0 },
|
||||
type: Actions.TOGGLE_DAY
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSequence()", () => {
|
||||
it("returns action", () => {
|
||||
const action = setSequence("sequences");
|
||||
expect(action).toEqual({
|
||||
payload: "sequences",
|
||||
type: Actions.SET_SEQUENCE
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { AddButtonProps } from "../interfaces";
|
||||
import { AddButton } from "../add_button";
|
||||
|
||||
describe("<AddButton />", () => {
|
||||
it("renders an add button when active", () => {
|
||||
const props: AddButtonProps = { active: true, click: jest.fn() };
|
||||
const wrapper = mount(<AddButton {...props} />);
|
||||
const button = wrapper.find("button");
|
||||
["green", "add"].map(klass => {
|
||||
expect(button.hasClass(klass)).toBeTruthy();
|
||||
});
|
||||
expect(wrapper.find("i").hasClass("fa-plus")).toBeTruthy();
|
||||
button.simulate("click");
|
||||
expect(props.click).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders a <div> when inactive", () => {
|
||||
const props: AddButtonProps = { active: false, click: jest.fn() };
|
||||
const wrapper = mount(<AddButton {...props} />);
|
||||
expect(wrapper.html()).toEqual("<div></div>");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { BulkSchedulerWidget } from "../index";
|
||||
import { BulkEditorProps } from "../interfaces";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { Actions } from "../../../constants";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<BulkSchedulerWidget />", () => {
|
||||
const weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
|
||||
function fakeProps(): BulkEditorProps {
|
||||
const sequence = fakeSequence();
|
||||
sequence.body.name = "Fake Sequence";
|
||||
return {
|
||||
selectedSequence: sequence,
|
||||
dailyOffsetMs: 3600000,
|
||||
weeks,
|
||||
sequences: [fakeSequence(), fakeSequence()],
|
||||
resources: buildResourceIndex([]).index,
|
||||
dispatch: jest.fn()
|
||||
};
|
||||
}
|
||||
|
||||
it("renders with sequence selected", () => {
|
||||
const wrapper = mount(<BulkSchedulerWidget {...fakeProps() } />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toEqual(6);
|
||||
["Scheduler", "Sequence", "Fake Sequence", "Time",
|
||||
"Days", "Week 1", "1234567"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("renders without sequence selected", () => {
|
||||
const p = fakeProps();
|
||||
p.selectedSequence = undefined;
|
||||
const wrapper = mount(<BulkSchedulerWidget {...p } />);
|
||||
["Sequence", "None", "Time"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("changes time", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkSchedulerWidget {...p } />);
|
||||
const timeInput = wrapper.find("BlurableInput").first();
|
||||
expect(timeInput.props().value).toEqual("01:00");
|
||||
timeInput.simulate("commit", { currentTarget: { value: "02:00" } });
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
payload: 7200000,
|
||||
type: Actions.SET_TIME_OFFSET
|
||||
});
|
||||
});
|
||||
|
||||
it("changes sequence", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkSchedulerWidget {...p } />);
|
||||
const sequenceInput = wrapper.find("FBSelect").first();
|
||||
sequenceInput.simulate("change", { value: "sequences" });
|
||||
expect(p.dispatch).toHaveBeenCalledWith({
|
||||
payload: "sequences",
|
||||
type: Actions.SET_SEQUENCE
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't change sequence", () => {
|
||||
const p = fakeProps();
|
||||
p.dispatch = jest.fn();
|
||||
const wrapper = shallow(<BulkSchedulerWidget {...p } />);
|
||||
const sequenceInput = wrapper.find("FBSelect").first();
|
||||
const change = () => sequenceInput.simulate("change", { value: 4 });
|
||||
expect(change).toThrowError("WARNING: Not a sequence UUID.");
|
||||
expect(p.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { WeekGrid } from "../week_grid";
|
||||
import { WeekGridProps } from "../interfaces";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<WeekGrid />", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const weeks = [{
|
||||
days:
|
||||
{
|
||||
day1: true,
|
||||
day2: false,
|
||||
day3: false,
|
||||
day4: false,
|
||||
day5: false,
|
||||
day6: false,
|
||||
day7: false
|
||||
}
|
||||
}];
|
||||
|
||||
it("renders", () => {
|
||||
const props: WeekGridProps = { weeks, dispatch: jest.fn() };
|
||||
const wrapper = mount(<WeekGrid {...props} />);
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toEqual(4);
|
||||
["Days", "Week 1", "1234567"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
function checkAction(position: number, text: string, type: Actions) {
|
||||
const props: WeekGridProps = { weeks, dispatch: jest.fn() };
|
||||
const wrapper = mount(<WeekGrid {...props} />);
|
||||
const button = wrapper.find("button").at(position);
|
||||
expect(button.text().toLowerCase()).toContain(text.toLowerCase());
|
||||
button.simulate("click");
|
||||
expect(props.dispatch).toHaveBeenCalledWith({ type, payload: undefined });
|
||||
}
|
||||
|
||||
it("adds week", () => {
|
||||
checkAction(0, "Week", Actions.PUSH_WEEK);
|
||||
});
|
||||
|
||||
it("removes week", () => {
|
||||
checkAction(1, "Week", Actions.POP_WEEK);
|
||||
});
|
||||
|
||||
it("selects all days", () => {
|
||||
checkAction(2, "Deselect All", Actions.DESELECT_ALL_DAYS);
|
||||
});
|
||||
|
||||
it("deselects all days", () => {
|
||||
checkAction(3, "Select All", Actions.SELECT_ALL_DAYS);
|
||||
});
|
||||
});
|
|
@ -73,9 +73,11 @@ export function commitBulkEditor(): Thunk {
|
|||
// Proceed only if they selected a sequence from the drop down.
|
||||
if (selectedSequenceUUID) {
|
||||
const seq = findSequence(res.index, selectedSequenceUUID).body;
|
||||
const regimenItems = groupRegimenItemsByWeek(weeks, dailyOffsetMs, seq);
|
||||
const regimenItems = weeks.length > 0
|
||||
? groupRegimenItemsByWeek(weeks, dailyOffsetMs, seq)
|
||||
: undefined;
|
||||
// Proceed only if days are selcted in the scheduler.
|
||||
if (regimenItems.length > 0) {
|
||||
if (regimenItems && regimenItems.length > 0) {
|
||||
const reg = findRegimen(res.index, currentRegimen);
|
||||
const update = defensiveClone(reg).body;
|
||||
update.regimen_items = update.regimen_items.concat(regimenItems);
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { ActiveEditor } from "../active_editor";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { ActiveEditorProps } from "../interfaces";
|
||||
import { Actions } from "../../../constants";
|
||||
|
||||
describe("<ActiveEditor />", () => {
|
||||
const props: ActiveEditorProps = {
|
||||
dispatch: jest.fn(),
|
||||
regimen: fakeRegimen(),
|
||||
calendar: [{
|
||||
day: "1",
|
||||
items: [{
|
||||
name: "Item 0",
|
||||
color: "red",
|
||||
hhmm: "10:00",
|
||||
sortKey: 0,
|
||||
day: 1,
|
||||
dispatch: jest.fn(),
|
||||
regimen: fakeRegimen(),
|
||||
item: {
|
||||
sequence_id: 0, time_offset: 1000
|
||||
}
|
||||
}]
|
||||
}]
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<ActiveEditor {...props} />);
|
||||
["Day", "Item 0", "10:00"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("removes regimen item", () => {
|
||||
const wrapper = mount(<ActiveEditor {...props} />);
|
||||
wrapper.find("i").simulate("click");
|
||||
expect(props.dispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
update: {
|
||||
color: "red", name: "Foo", regimen_items: []
|
||||
},
|
||||
uuid: "regimens.1.17"
|
||||
},
|
||||
type: Actions.OVERWRITE_RESOURCE
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
import { fakeState } from "../../../__test_support__/fake_state";
|
||||
const mockState = fakeState;
|
||||
const mockDestroy = jest.fn();
|
||||
const mockSave = jest.fn();
|
||||
jest.mock("../../../api/crud", () => ({
|
||||
getState: mockState,
|
||||
destroy: mockDestroy,
|
||||
save: mockSave
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { RegimenEditorWidget } from "../index";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { RegimenEditorWidgetProps } from "../interfaces";
|
||||
import { auth } from "../../../__test_support__/fake_state/token";
|
||||
import { bot } from "../../../__test_support__/fake_state/bot";
|
||||
|
||||
describe("<RegimenEditorWidget />", () => {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function fakeProps(): RegimenEditorWidgetProps {
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
auth,
|
||||
bot,
|
||||
current: fakeRegimen(),
|
||||
calendar: [{
|
||||
day: "1",
|
||||
items: [{
|
||||
name: "Item 0",
|
||||
color: "red",
|
||||
hhmm: "10:00",
|
||||
sortKey: 0,
|
||||
day: 1,
|
||||
dispatch: jest.fn(),
|
||||
regimen: fakeRegimen(),
|
||||
item: {
|
||||
sequence_id: 0, time_offset: 1000
|
||||
}
|
||||
}]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
it("active editor", () => {
|
||||
const wrapper = mount(<RegimenEditorWidget {...fakeProps() } />);
|
||||
["Regimen Editor", "Delete", "Item 0", "10:00"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("empty editor", () => {
|
||||
const props = fakeProps();
|
||||
props.current = undefined;
|
||||
const wrapper = mount(<RegimenEditorWidget {...props} />);
|
||||
["Regimen Editor", "No Regimen selected."].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
|
||||
it("error: not logged in", () => {
|
||||
const props = fakeProps();
|
||||
props.auth = undefined;
|
||||
const wrapper = () => mount(<RegimenEditorWidget {...props} />);
|
||||
expect(wrapper).toThrowError("Must log in first");
|
||||
});
|
||||
|
||||
it("deletes regimen", () => {
|
||||
const wrapper = mount(<RegimenEditorWidget {...fakeProps() } />);
|
||||
const deleteButton = wrapper.find("button").at(2);
|
||||
expect(deleteButton.text()).toContain("Delete");
|
||||
deleteButton.simulate("click");
|
||||
expect(mockDestroy).toHaveBeenCalledWith("regimens.6.22");
|
||||
});
|
||||
|
||||
it("saves regimen", () => {
|
||||
const wrapper = mount(<RegimenEditorWidget {...fakeProps() } />);
|
||||
const saveeButton = wrapper.find("button").at(0);
|
||||
expect(saveeButton.text()).toContain("Save");
|
||||
saveeButton.simulate("click");
|
||||
expect(mockSave).toHaveBeenCalledWith("regimens.8.24");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
jest.mock("../../actions", () => ({ editRegimen: jest.fn() }));
|
||||
|
||||
import { write } from "../regimen_name_input";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
import { editRegimen } from "../../actions";
|
||||
|
||||
describe("write()", () => {
|
||||
it("crashes without a regimen", () => {
|
||||
const input = { regimen: undefined, dispatch: jest.fn() };
|
||||
expect(() => write(input)).toThrowError();
|
||||
});
|
||||
|
||||
it("calls dispatch", () => {
|
||||
const input = { regimen: fakeRegimen(), dispatch: jest.fn() };
|
||||
const callback = write(input);
|
||||
expect(callback).toBeInstanceOf(Function);
|
||||
const value = "FOO";
|
||||
callback({ currentTarget: { value } } as any);
|
||||
expect(input.dispatch).toHaveBeenCalled();
|
||||
expect(editRegimen).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@ import { ColorPicker } from "../../ui";
|
|||
import { Row, Col } from "../../ui/index";
|
||||
import { editRegimen } from "../actions";
|
||||
|
||||
function write({ dispatch, regimen }: RegimenProps):
|
||||
export function write({ dispatch, regimen }: RegimenProps):
|
||||
React.EventHandler<React.FormEvent<{}>> {
|
||||
if (regimen) {
|
||||
return (event: React.FormEvent<HTMLInputElement>) => {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import * as React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import { RegimensList } from "../index";
|
||||
import { RegimensListProps } from "../../interfaces";
|
||||
import { fakeRegimen } from "../../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<RegimensList />", () => {
|
||||
function fakeProps(): RegimensListProps {
|
||||
const regimen = fakeRegimen();
|
||||
regimen.body.name = "Fake Regimen";
|
||||
return {
|
||||
dispatch: jest.fn(),
|
||||
regimens: [regimen, regimen],
|
||||
regimen: undefined
|
||||
};
|
||||
}
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<RegimensList {...fakeProps() } />);
|
||||
expect(wrapper.text()).toContain("Regimens");
|
||||
expect(wrapper.text()).toContain("Fake Regimen Fake Regimen");
|
||||
});
|
||||
|
||||
it("sets search term", () => {
|
||||
const wrapper = shallow(<RegimensList {...fakeProps() } />);
|
||||
wrapper.find("input").simulate("change",
|
||||
{ currentTarget: { value: "term" } });
|
||||
expect(wrapper.state().searchTerm).toEqual("term");
|
||||
});
|
||||
});
|
|
@ -17,7 +17,7 @@ describe("<SequencesList />", () => {
|
|||
sequences={[fakeSequence1, fakeSequence2]} />);
|
||||
expect(wrapper.find("input").first().props().placeholder)
|
||||
.toContain("Search Sequences");
|
||||
expect(wrapper.text()).toContain("Sequence 1");
|
||||
expect(wrapper.text()).toContain("Sequence 2");
|
||||
["Sequence 1", "Sequence 2"].map(string =>
|
||||
expect(wrapper.text()).toContain(string));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,7 +32,7 @@ export function fetchSyncData(dispatch: Function) {
|
|||
type, payload: { name, data: r.data }
|
||||
}), fail);
|
||||
|
||||
const fail = () => warning("Please try refreshing the page.",
|
||||
const fail = () => warning("Please try refreshing the page or logging in again.",
|
||||
"Error downloading data");
|
||||
|
||||
fetch<User>("users", API.current.usersPath);
|
||||
|
@ -47,16 +47,3 @@ export function fetchSyncData(dispatch: Function) {
|
|||
fetch<Sequence[]>("sequences", API.current.sequencesPath);
|
||||
fetch<Tool[]>("tools", API.current.toolsPath);
|
||||
}
|
||||
|
||||
export function fetchSyncDataOk(payload: {}) {
|
||||
return {
|
||||
type: "FETCH_SYNC_OK", payload
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSyncDataNo(err: Error) {
|
||||
return {
|
||||
type: "FETCH_SYNC_NO",
|
||||
payload: {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
jest.mock("react-redux", () => ({
|
||||
connect: jest.fn()
|
||||
}));
|
||||
|
||||
import * as React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { Tools } from "../index";
|
||||
import { Props } from "../interfaces";
|
||||
import { fakeToolSlot, fakeTool } from "../../__test_support__/fake_state/resources";
|
||||
|
||||
describe("<Tools />", () => {
|
||||
function fakeProps(): Props {
|
||||
return {
|
||||
toolSlots: [],
|
||||
tools: [fakeTool()],
|
||||
getToolSlots: () => [fakeToolSlot()],
|
||||
getToolOptions: () => [],
|
||||
getChosenToolOption: () => { return { label: "None", value: "" }; },
|
||||
getToolByToolSlotUUID: () => fakeTool(),
|
||||
changeToolSlot: jest.fn(),
|
||||
isActive: () => true,
|
||||
dispatch: jest.fn(),
|
||||
botPosition: { x: undefined, y: undefined, z: undefined }
|
||||
};
|
||||
}
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = mount(<Tools {...fakeProps() } />);
|
||||
const txt = wrapper.text();
|
||||
const strings = [
|
||||
"ToolBay 1",
|
||||
"SlotXYZ",
|
||||
"Tool1101010Foo",
|
||||
"Tools",
|
||||
"Tool NameStatus",
|
||||
"Fooactive"];
|
||||
strings.map(string => expect(txt).toContain(string));
|
||||
});
|
||||
});
|
|
@ -28,7 +28,7 @@ export class ToolList extends React.Component<ToolListProps, {}> {
|
|||
</Col>
|
||||
</Row>
|
||||
{tools.map((tool: TaggedTool) => {
|
||||
return <Row key={tool.body.id}>
|
||||
return <Row key={tool.uuid}>
|
||||
<Col xs={8}>
|
||||
{tool.body.name || "Name not found"}
|
||||
</Col>
|
||||
|
|
|
@ -24,7 +24,7 @@ export class ToolBayList extends React.Component<ToolBayListProps, {}> {
|
|||
{getToolSlots().map((slot: TaggedToolSlotPointer, index: number) => {
|
||||
const tool = getToolByToolSlotUUID(slot.uuid);
|
||||
const name = (tool && tool.body.name) || "None";
|
||||
return <Row key={slot.body.id}>
|
||||
return <Row key={slot.uuid}>
|
||||
<Col xs={2}>
|
||||
<label>{index + 1}</label>
|
||||
</Col>
|
||||
|
|
|
@ -461,5 +461,5 @@ export function withTimeout<T>(ms: number, promise: Promise<T>) {
|
|||
});
|
||||
|
||||
// Returns a race between our timeout and the passed in promise
|
||||
return Promise.race([promise, timeout]);
|
||||
return Promise.race([promise, timeout]) as Promise<T>;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import { getParam, HttpData } from "./util";
|
||||
import axios from "axios";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { API } from "./api/api";
|
||||
import { Session } from "./session";
|
||||
import { AuthState } from "./auth/interfaces";
|
||||
|
||||
/** Keep track of this in rollbar to prevent global registration failures. */
|
||||
export const ALREADY_VERIFIED =
|
||||
`<p>
|
||||
You are already verified. We will now forward you to the main application.
|
||||
</p>
|
||||
<p>
|
||||
If you are still unable to access the app, try logging in again or
|
||||
<a href="http://forum.farmbot.org/"> asking for help on the FarmBot Forum.</a>
|
||||
</p>`;
|
||||
const ALREADY_VERIFIED_MSG = "TRIED TO RE-VERIFY";
|
||||
|
||||
export const FAILURE_PAGE =
|
||||
`<p>
|
||||
We were unable to verify your account.
|
||||
|
@ -17,7 +28,15 @@ export const FAILURE_MSG = "USER VERIFICATION FAILED!";
|
|||
|
||||
/** Function called when the Frontend verifies its registration token.
|
||||
* IF YOU BREAK THIS FUNCTION, YOU BREAK *ALL* NEW USER REGISTRATIONS. */
|
||||
export const verify = async () => { try { attempt(); } catch (e) { fail(); } };
|
||||
export const verify = async () => {
|
||||
try {
|
||||
console.log("TODO: Make sure this thing actually uses `await`." +
|
||||
" function won't work without await");
|
||||
await attempt();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
};
|
||||
|
||||
export async function attempt() {
|
||||
API.setBaseUrl(API.fetchBrowserLocation());
|
||||
|
@ -28,7 +47,23 @@ export async function attempt() {
|
|||
window.location.href = API.current.baseUrl + "/app/controls";
|
||||
}
|
||||
|
||||
export function fail() {
|
||||
document.write(FAILURE_PAGE);
|
||||
throw new Error(FAILURE_MSG);
|
||||
interface AxiosError extends Error {
|
||||
response?: AxiosResponse | undefined; // Need to be extra cautious here.
|
||||
}
|
||||
|
||||
export function fail(err: AxiosError | undefined) {
|
||||
switch (err && err.response && err.response.status) {
|
||||
case 409:
|
||||
alreadyVerified();
|
||||
break;
|
||||
default:
|
||||
document.write(FAILURE_PAGE);
|
||||
throw new Error(FAILURE_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
const alreadyVerified = () => {
|
||||
window.location.href = "/app/controls";
|
||||
document.write(ALREADY_VERIFIED);
|
||||
throw new Error(ALREADY_VERIFIED_MSG);
|
||||
};
|
||||
|
|
|
@ -1972,9 +1972,9 @@ farmbot-toastr@^1.0.0, farmbot-toastr@^1.0.3:
|
|||
farmbot-toastr "^1.0.0"
|
||||
typescript "^2.3.4"
|
||||
|
||||
farmbot@5.0.1-rc12:
|
||||
version "5.0.1-rc12"
|
||||
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc12.tgz#3746018e2d42657ece67ff2af23b507d604ceb0d"
|
||||
farmbot@5.0.1-rc13:
|
||||
version "5.0.1-rc13"
|
||||
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc13.tgz#473366b179eb967d9f1265952334b6195de7b1ef"
|
||||
dependencies:
|
||||
mqtt "^1.7.4"
|
||||
typescript "^2.4.2"
|
||||
|
|
Loading…
Reference in New Issue