Merge conflicts
This commit is contained in:
commit
b977825e0c
|
@ -13,6 +13,9 @@ module Api
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
after_action :skip_set_cookies_header
|
after_action :skip_set_cookies_header
|
||||||
|
|
||||||
|
rescue_from(User::AlreadyVerified) { sorry "Already verified.", 409 }
|
||||||
|
|
||||||
rescue_from(JWT::VerificationError) { |e| auth_err }
|
rescue_from(JWT::VerificationError) { |e| auth_err }
|
||||||
|
|
||||||
rescue_from(ActionDispatch::Http::Parameters::ParseError) do
|
rescue_from(ActionDispatch::Http::Parameters::ParseError) do
|
||||||
|
@ -23,6 +26,7 @@ module Api
|
||||||
rescue_from(ActiveRecord::ValueTooLong) do
|
rescue_from(ActiveRecord::ValueTooLong) do
|
||||||
sorry "Please use reasonable lengths on string inputs", 422
|
sorry "Please use reasonable lengths on string inputs", 422
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from Errors::Forbidden do |exc|
|
rescue_from Errors::Forbidden do |exc|
|
||||||
sorry "You can't perform that action. #{exc.message}", 403
|
sorry "You can't perform that action. #{exc.message}", 403
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# A human
|
# A human
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
class AlreadyVerified < StandardError; end
|
||||||
ENFORCE_TOS = ENV.fetch("TOS_URL") { false }
|
ENFORCE_TOS = ENV.fetch("TOS_URL") { false }
|
||||||
SKIP_EMAIL_VALIDATION = ENV.fetch("NO_EMAILS") { false }
|
SKIP_EMAIL_VALIDATION = ENV.fetch("NO_EMAILS") { false }
|
||||||
validates :email, uniqueness: true
|
validates :email, uniqueness: true
|
||||||
|
@ -32,4 +33,22 @@ class User < ApplicationRecord
|
||||||
def verified?
|
def verified?
|
||||||
SKIP_EMAIL_VALIDATION ? true : !!confirmed_at
|
SKIP_EMAIL_VALIDATION ? true : !!confirmed_at
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -7,22 +7,9 @@ module Auth
|
||||||
def execute
|
def execute
|
||||||
token = SessionToken.decode!(just_the_token)
|
token = SessionToken.decode!(just_the_token)
|
||||||
claims = token.unencoded
|
claims = token.unencoded
|
||||||
sub = claims["sub"]
|
User.find_by_email_or_id(claims["sub"])
|
||||||
case sub
|
rescue JWT::DecodeError, ActiveRecord::RecordNotFound, User::BadSub
|
||||||
when Integer then User.find(sub)
|
add_error :jwt, :decode_error, Auth::ReloadToken::BAD_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."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def just_the_token
|
def just_the_token
|
||||||
|
|
|
@ -2,11 +2,17 @@ module Auth
|
||||||
# The API supports a number of authentication strategies (Cookies, Bot token,
|
# The API supports a number of authentication strategies (Cookies, Bot token,
|
||||||
# JWT). This service helps determine which auth strategy to use.
|
# JWT). This service helps determine which auth strategy to use.
|
||||||
class ReloadToken < Mutations::Command
|
class ReloadToken < Mutations::Command
|
||||||
|
attr_reader :user
|
||||||
|
BAD_SUB = "Please log out and try again."
|
||||||
|
|
||||||
required { string :jwt }
|
required { string :jwt }
|
||||||
|
|
||||||
|
def validate
|
||||||
|
@user = User.find_by_email_or_id(claims["sub"])
|
||||||
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
# Prevent never ending sessions.
|
security_criticial_danger = claims["exp"] # Stop infinite sessions
|
||||||
security_criticial_danger = claims["exp"]
|
|
||||||
token = SessionToken.issue_to(user,
|
token = SessionToken.issue_to(user,
|
||||||
aud: claims["aud"],
|
aud: claims["aud"],
|
||||||
exp: security_criticial_danger)
|
exp: security_criticial_danger)
|
||||||
|
@ -17,10 +23,6 @@ module Auth
|
||||||
@claims ||= SessionToken.decode!(jwt.split(" ").last).unencoded
|
@claims ||= SessionToken.decode!(jwt.split(" ").last).unencoded
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
|
||||||
@user ||= User.find(claims["sub"])
|
|
||||||
end
|
|
||||||
|
|
||||||
def nope
|
def nope
|
||||||
add_error :jwt, :decode_error, "JSON Web Token is not valid."
|
add_error :jwt, :decode_error, "JSON Web Token is not valid."
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,17 +3,21 @@ module Users
|
||||||
required { string :token, min_length: 5 }
|
required { string :token, min_length: 5 }
|
||||||
|
|
||||||
def validate
|
def validate
|
||||||
|
prevent_token_reuse
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
user.confirmed_at = Time.now
|
user.verified_at = Time.now
|
||||||
# Prevent token reuse:
|
|
||||||
user.confirmation_token = ""
|
|
||||||
user.save!
|
user.save!
|
||||||
SessionToken.as_json(user.reload, AbstractJwtToken::HUMAN_AUD)
|
SessionToken.as_json(user.reload, AbstractJwtToken::HUMAN_AUD)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def prevent_token_reuse
|
||||||
|
raise User::AlreadyVerified if user.verified_at.present?
|
||||||
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
@user ||= User.find_by!(confirmation_token: token)
|
@user ||= User.find_by!(confirmation_token: token)
|
||||||
end
|
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/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 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">
|
<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 %>
|
<% if false %>
|
||||||
<script async src="http://<%= ENV["API_HOST"] %>:8080/target/target-script-min.js#anonymous">
|
<script async src="http://<%= ENV["API_HOST"] %>:8080/target/target-script-min.js#anonymous">
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@ var devServerPort = 3808;
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
var genConfig = require("./webpack.base");
|
var genConfig = require("./webpack.base");
|
||||||
var conf = genConfig();
|
var conf = genConfig();
|
||||||
var webpack = require("webpack");
|
|
||||||
|
|
||||||
conf.output = {
|
conf.output = {
|
||||||
// must match config.webpack.output_dir
|
// must match config.webpack.output_dir
|
||||||
|
|
|
@ -19,7 +19,18 @@ conf.output = {
|
||||||
[
|
[
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: "commons",
|
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({
|
new ExtractTextPlugin({
|
||||||
// Temporary hotfix for some issues on staging.
|
// Temporary hotfix for some issues on staging.
|
||||||
|
|
18
package.json
18
package.json
|
@ -29,10 +29,8 @@
|
||||||
"@blueprintjs/core": "^1.29.0",
|
"@blueprintjs/core": "^1.29.0",
|
||||||
"@blueprintjs/datetime": "^1.22.0",
|
"@blueprintjs/datetime": "^1.22.0",
|
||||||
"@blueprintjs/labs": "^0.4.0",
|
"@blueprintjs/labs": "^0.4.0",
|
||||||
"@types/deep-freeze": "^0.1.1",
|
|
||||||
"@types/enzyme": "^2.8.9",
|
"@types/enzyme": "^2.8.9",
|
||||||
"@types/fastclick": "^1.0.28",
|
"@types/fastclick": "^1.0.28",
|
||||||
"@types/handlebars": "^4.0.36",
|
|
||||||
"@types/history": "^4.6.0",
|
"@types/history": "^4.6.0",
|
||||||
"@types/i18next": "^8.4.2",
|
"@types/i18next": "^8.4.2",
|
||||||
"@types/jest": "^21.1.2",
|
"@types/jest": "^21.1.2",
|
||||||
|
@ -41,37 +39,34 @@
|
||||||
"@types/moxios": "^0.4.5",
|
"@types/moxios": "^0.4.5",
|
||||||
"@types/mqtt": "^0.0.34",
|
"@types/mqtt": "^0.0.34",
|
||||||
"@types/node": "^8.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-color": "^2.13.2",
|
||||||
"@types/react-dom": "^15.5.5",
|
"@types/react-dom": "^15.5.5",
|
||||||
"@types/react-redux": "^4.4.47",
|
"@types/react-redux": "^4.4.47",
|
||||||
"@types/react-router": "3",
|
"@types/react-router": "3",
|
||||||
"@types/react": "15.0.39",
|
|
||||||
"@types/redux": "^3.6.31",
|
"@types/redux": "^3.6.31",
|
||||||
"axios": "^0.16.2",
|
"axios": "^0.16.2",
|
||||||
"boxed_value": "^1.0.0",
|
"boxed_value": "^1.0.0",
|
||||||
"coveralls": "^3.0.0",
|
"coveralls": "^3.0.0",
|
||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
"deep-freeze": "^0.0.1",
|
|
||||||
"enzyme": "^2.9.1",
|
"enzyme": "^2.9.1",
|
||||||
"extract-text-webpack-plugin": "^3.0.1",
|
"extract-text-webpack-plugin": "^3.0.1",
|
||||||
|
"farmbot": "5.0.1-rc13",
|
||||||
"farmbot-toastr": "^1.0.3",
|
"farmbot-toastr": "^1.0.3",
|
||||||
"farmbot": "5.0.1-rc12",
|
|
||||||
"fastclick": "^1.0.6",
|
"fastclick": "^1.0.6",
|
||||||
"file-loader": "^1.1.5",
|
"file-loader": "^1.1.5",
|
||||||
"handlebars": "^4.0.10",
|
|
||||||
"i18next": "^9.0.0",
|
"i18next": "^9.0.0",
|
||||||
"imports-loader": "^0.7.0",
|
"imports-loader": "^0.7.0",
|
||||||
"jest": "^21.2.1",
|
"jest": "^21.2.1",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
|
||||||
"markdown-it": "^8.4.0",
|
"markdown-it": "^8.4.0",
|
||||||
|
"markdown-it-emoji": "^1.4.0",
|
||||||
"moment": "^2.19.1",
|
"moment": "^2.19.1",
|
||||||
"moxios": "^0.4.0",
|
"moxios": "^0.4.0",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
"offline-js": "^0.7.19",
|
|
||||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||||
|
"react": "^15.6.1",
|
||||||
"react-addons-css-transition-group": "^15.6.2",
|
"react-addons-css-transition-group": "^15.6.2",
|
||||||
"react-addons-test-utils": "^15.6.2",
|
"react-addons-test-utils": "^15.6.2",
|
||||||
"react-color": "^2.13.8",
|
"react-color": "^2.13.8",
|
||||||
|
@ -79,10 +74,9 @@
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"react-router": "^3.0.0",
|
"react-router": "^3.0.0",
|
||||||
"react-test-renderer": "^15.6.1",
|
"react-test-renderer": "^15.6.1",
|
||||||
"react": "^15.6.1",
|
"redux": "^3.7.2",
|
||||||
"redux-immutable-state-invariant": "^2.1.0",
|
"redux-immutable-state-invariant": "^2.1.0",
|
||||||
"redux-thunk": "^2.0.1",
|
"redux-thunk": "^2.0.1",
|
||||||
"redux": "^3.7.2",
|
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"stats-webpack-plugin": "^0.6.1",
|
"stats-webpack-plugin": "^0.6.1",
|
||||||
"style-loader": "^0.19.0",
|
"style-loader": "^0.19.0",
|
||||||
|
@ -92,8 +86,8 @@
|
||||||
"tslint": "^5.7.0",
|
"tslint": "^5.7.0",
|
||||||
"typescript": "^2.5.3",
|
"typescript": "^2.5.3",
|
||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"webpack-uglify-js-plugin": "^1.1.9",
|
|
||||||
"webpack": "^3.7.1",
|
"webpack": "^3.7.1",
|
||||||
|
"webpack-uglify-js-plugin": "^1.1.9",
|
||||||
"weinre": "^2.0.0-pre-I0Z7U9OV",
|
"weinre": "^2.0.0-pre-I0Z7U9OV",
|
||||||
"which": "^1.3.0",
|
"which": "^1.3.0",
|
||||||
"yarn": "^1.2.1"
|
"yarn": "^1.2.1"
|
||||||
|
|
|
@ -30,7 +30,26 @@ describe Api::TokensController do
|
||||||
it 'denies bad tokens' do
|
it 'denies bad tokens' do
|
||||||
request.headers["Authorization"] = "bearer #{auth_token.encoded + "no..."}"
|
request.headers["Authorization"] = "bearer #{auth_token.encoded + "no..."}"
|
||||||
get :show
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,6 +111,13 @@ describe Api::UsersController do
|
||||||
end
|
end
|
||||||
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
|
it 'handles password confirmation mismatch' do
|
||||||
email = Faker::Internet.email
|
email = Faker::Internet.email
|
||||||
original_count = User.count
|
original_count = User.count
|
||||||
|
|
|
@ -9,8 +9,8 @@ describe Api::UsersController do
|
||||||
expect(user.confirmed_at).to eq(nil)
|
expect(user.confirmed_at).to eq(nil)
|
||||||
put :verify, params: params
|
put :verify, params: params
|
||||||
user.reload
|
user.reload
|
||||||
expect(user.confirmation_token).to eq("")
|
expect(user.verification_token).to eq(params[:token])
|
||||||
expect(user.confirmed_at).to be
|
expect(user.verified_at).to be
|
||||||
expect(user.confirmed_at - Time.now).to be < 3
|
expect(user.verified_at - Time.now).to be < 3
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,8 +38,10 @@ describe SessionToken do
|
||||||
token = SessionToken.issue_to(user, iat: 000, exp: 1, iss: "//lycos.com:9867")
|
token = SessionToken.issue_to(user, iat: 000, exp: 1, iss: "//lycos.com:9867")
|
||||||
result = Auth::FromJWT.run(jwt: token.encoded)
|
result = Auth::FromJWT.run(jwt: token.encoded)
|
||||||
expect(result.success?).to be(false)
|
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
|
end
|
||||||
|
|
||||||
unless ENV["NO_EMAILS"]
|
unless ENV["NO_EMAILS"]
|
||||||
it "doesn't mint tokens for unverified users" do
|
it "doesn't mint tokens for unverified users" do
|
||||||
user.update_attributes!(confirmed_at: nil)
|
user.update_attributes!(confirmed_at: nil)
|
||||||
|
|
|
@ -26,8 +26,8 @@ describe Auth::FromJWT do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "crashes when sub is neither string nor Integer" do
|
it "crashes when sub is neither string nor Integer" do
|
||||||
t = fake[1.23]
|
expect {
|
||||||
bad_sub = "SUB was neither string nor number"
|
Auth::FromJWT.run!(jwt: fake[1.23])
|
||||||
expect { Auth::FromJWT.run!(jwt: t) }.to raise_error(bad_sub)
|
}.to raise_error(Auth::ReloadToken::BAD_SUB)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { buildResourceIndex } from "../resource_index_builder";
|
||||||
import {
|
import {
|
||||||
TaggedFarmEvent, TaggedSequence, TaggedRegimen, TaggedImage,
|
TaggedFarmEvent, TaggedSequence, TaggedRegimen, TaggedImage,
|
||||||
TaggedTool, TaggedToolSlotPointer, TaggedUser, TaggedWebcamFeed,
|
TaggedTool, TaggedToolSlotPointer, TaggedUser, TaggedWebcamFeed,
|
||||||
TaggedPlantPointer, TaggedGenericPointer
|
TaggedPlantPointer, TaggedGenericPointer, TaggedPeripheral
|
||||||
} from "../../resources/tagged_resources";
|
} from "../../resources/tagged_resources";
|
||||||
import { ExecutableType } from "../../farm_designer/interfaces";
|
import { ExecutableType } from "../../farm_designer/interfaces";
|
||||||
import { fakeResource } from "../fake_resource";
|
import { fakeResource } from "../fake_resource";
|
||||||
|
@ -123,3 +123,11 @@ export function fakeWebcamFeed(): TaggedWebcamFeed {
|
||||||
name: "wcf #" + id
|
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": {
|
"token": {
|
||||||
"unencoded": {
|
"unencoded": {
|
||||||
"iat": 1495569084,
|
|
||||||
"jti": "b38915ca-3d7a-4754-8152-d4306b88504c",
|
|
||||||
"iss": "//localhost:3000",
|
"iss": "//localhost:3000",
|
||||||
"exp": 1499025084,
|
|
||||||
"mqtt": "10.0.0.6",
|
|
||||||
"os_update_server": "https://api.github.com/repos/farmbot/" +
|
"os_update_server": "https://api.github.com/repos/farmbot/" +
|
||||||
"farmbot_os/releases/latest"
|
"farmbot_os/releases/latest"
|
||||||
},
|
},
|
||||||
|
@ -24,7 +20,6 @@ export let auth: Everything["auth"] = {
|
||||||
"XV6SGE624zdr7S7mQ6uj7qpa2LMH4P37R3BIB26G7E8xDcVOGqL5Oiwr9DPajBX3zd" +
|
"XV6SGE624zdr7S7mQ6uj7qpa2LMH4P37R3BIB26G7E8xDcVOGqL5Oiwr9DPajBX3zd" +
|
||||||
"hXSbH3k4PyxqvPOLYso-R7kjfpOnfFCMfMZLW8TQtg-yj82zs93RP2DHOOx-jxek69" +
|
"hXSbH3k4PyxqvPOLYso-R7kjfpOnfFCMfMZLW8TQtg-yj82zs93RP2DHOOx-jxek69" +
|
||||||
"tmgNyP3FJaoWHwHW7bXOEv09p3dhNVTCSVNKD9LZczLpuXV7U4oSmL6KLkbzsM6G0P" +
|
"tmgNyP3FJaoWHwHW7bXOEv09p3dhNVTCSVNKD9LZczLpuXV7U4oSmL6KLkbzsM6G0P" +
|
||||||
|
|
||||||
"9rrbJ9ASYaOw"
|
"9rrbJ9ASYaOw"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ jest.mock("../session", () => {
|
||||||
|
|
||||||
import { maybeRefreshToken } from "../refresh_token";
|
import { maybeRefreshToken } from "../refresh_token";
|
||||||
import { API } from "../api/index";
|
import { API } from "../api/index";
|
||||||
import { Session } from "../session";
|
|
||||||
|
|
||||||
API.setBaseUrl("http://blah.whatever.party");
|
API.setBaseUrl("http://blah.whatever.party");
|
||||||
|
|
||||||
|
@ -30,8 +29,6 @@ describe("maybeRefreshToken()", () => {
|
||||||
token: {
|
token: {
|
||||||
encoded: "---",
|
encoded: "---",
|
||||||
unencoded: {
|
unencoded: {
|
||||||
iat: 123,
|
|
||||||
jti: "111",
|
|
||||||
iss: "---",
|
iss: "---",
|
||||||
exp: 456,
|
exp: 456,
|
||||||
mqtt: "---",
|
mqtt: "---",
|
||||||
|
@ -39,8 +36,8 @@ describe("maybeRefreshToken()", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
maybeRefreshToken(t).then(() => {
|
maybeRefreshToken(t).then((result) => {
|
||||||
expect(Session.clear).toHaveBeenCalled();
|
expect(result).toBeUndefined();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
const mockAuth = (jti = "456"): AuthState => ({
|
const mockAuth = (iss = "987"): AuthState => ({
|
||||||
token: {
|
token: {
|
||||||
encoded: "---",
|
encoded: "---",
|
||||||
unencoded: {
|
unencoded: { iss, os_update_server: "---" }
|
||||||
iat: 123,
|
|
||||||
jti,
|
|
||||||
iss: "---",
|
|
||||||
exp: 456,
|
|
||||||
mqtt: "---",
|
|
||||||
os_update_server: "---"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,8 +27,12 @@ describe("maybeRefreshToken()", () => {
|
||||||
it("gives you back your token when things fail", (done) => {
|
it("gives you back your token when things fail", (done) => {
|
||||||
maybeRefreshToken(mockAuth("111"))
|
maybeRefreshToken(mockAuth("111"))
|
||||||
.then((nextToken) => {
|
.then((nextToken) => {
|
||||||
expect(nextToken.token.unencoded.jti).toEqual("000");
|
if (nextToken) {
|
||||||
|
expect(nextToken.token.unencoded.iss).toEqual("000");
|
||||||
done();
|
done();
|
||||||
|
} else {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ jest.mock("../util", () => ({ getParam: () => "STUB_PARAM" }));
|
||||||
|
|
||||||
jest.mock("axios", () => ({
|
jest.mock("axios", () => ({
|
||||||
default: {
|
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 { API } from "../api/api";
|
||||||
import { Session } from "../session";
|
import { Session } from "../session";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getParam } from "../util";
|
import { getParam } from "../util";
|
||||||
|
|
||||||
describe("fail()", () => {
|
describe("fail()", () => {
|
||||||
it("writes a failure message", () => {
|
it("writes a failure message - base case", () => {
|
||||||
expect(fail).toThrow();
|
expect(fail).toThrow();
|
||||||
expect(document.documentElement.outerHTML).toContain(FAILURE_PAGE);
|
expect(document.documentElement.outerHTML).toContain(FAILURE_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("writes a failure message - base case", () => {
|
||||||
|
expect(() => fail({ response: { status: 409 } } as any)).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("attempt()", () => {
|
describe("attempt()", () => {
|
||||||
|
|
105
webpack/auth/__tests__/actions_test.ts
Normal file
105
webpack/auth/__tests__/actions_test.ts
Normal file
|
@ -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,
|
confirmation: string,
|
||||||
url: string): Thunk {
|
url: string): Thunk {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
const p = requestRegistration(name,
|
return requestRegistration(name, email, password, confirmation)
|
||||||
email,
|
.then(onLogin(dispatch), onRegistrationErr(dispatch));
|
||||||
password,
|
|
||||||
confirmation,
|
|
||||||
url);
|
|
||||||
return p.then(onLogin(dispatch),
|
|
||||||
onRegistrationErr(dispatch));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,24 +82,17 @@ export function onRegistrationErr(dispatch: Function) {
|
||||||
|
|
||||||
/** Build a JSON object in preparation for an HTTP POST
|
/** Build a JSON object in preparation for an HTTP POST
|
||||||
* to registration endpoint */
|
* to registration endpoint */
|
||||||
function requestRegistration(name: string,
|
export function requestRegistration(name: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
confirmation: string,
|
password_confirmation: string) {
|
||||||
url: string) {
|
|
||||||
const form = {
|
const form = { user: { email, password, password_confirmation, name } };
|
||||||
user: {
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
password_confirmation: confirmation,
|
|
||||||
name: name
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return axios.post(API.current.usersPath, form);
|
return axios.post(API.current.usersPath, form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch API token if already registered. */
|
/** Fetch API token if already registered. */
|
||||||
function requestToken(email: string,
|
export function requestToken(email: string,
|
||||||
password: string,
|
password: string,
|
||||||
url: string) {
|
url: string) {
|
||||||
const payload = { user: { email: email, password: password } };
|
const payload = { user: { email: email, password: password } };
|
||||||
|
|
|
@ -8,16 +8,10 @@ export interface AuthState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnencodedToken {
|
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). */
|
/** ISSUER - Where token came from (API URL). */
|
||||||
iss: string;
|
iss: string;
|
||||||
/** EXPIRATION DATE */
|
|
||||||
exp: number;
|
|
||||||
/** MQTT server address */
|
/** MQTT server address */
|
||||||
mqtt: string;
|
// mqtt: string;
|
||||||
/** Where to download RPi software */
|
/** Where to download RPi software */
|
||||||
os_update_server: string;
|
os_update_server: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,15 @@ import { maybeRefreshToken } from "../refresh_token";
|
||||||
import { withTimeout } from "../util";
|
import { withTimeout } from "../util";
|
||||||
import { AuthState } from "../auth/interfaces";
|
import { AuthState } from "../auth/interfaces";
|
||||||
|
|
||||||
export const storeToken = (auth: AuthState, dispatch: Function) => () => {
|
export const storeToken =
|
||||||
dispatch(setToken(auth));
|
(old: AuthState, dispatch: Function) => (_new: AuthState | undefined) => {
|
||||||
didLogin(auth, dispatch);
|
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
|
/** Amount of time we're willing to wait before concluding that the token is bad
|
||||||
* or the API is down. */
|
* or the API is down. */
|
||||||
|
@ -22,8 +27,10 @@ export function ready(): Thunk {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const auth = Session.fetchStoredToken() || getState().auth;
|
const auth = Session.fetchStoredToken() || getState().auth;
|
||||||
if (auth) {
|
if (auth) {
|
||||||
withTimeout(MAX_TOKEN_WAIT_TIME, maybeRefreshToken(auth))
|
const ok = storeToken(auth, dispatch);
|
||||||
.then(storeToken(auth, dispatch), Session.clear);
|
const no = () => ok(undefined);
|
||||||
|
const p = maybeRefreshToken(auth);
|
||||||
|
withTimeout(MAX_TOKEN_WAIT_TIME, p).then(ok, no);
|
||||||
} else {
|
} else {
|
||||||
Session.clear();
|
Session.clear();
|
||||||
}
|
}
|
||||||
|
|
74
webpack/controls/peripherals/__tests__/index_test.tsx
Normal file
74
webpack/controls/peripherals/__tests__/index_test.tsx
Normal file
|
@ -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 version = resp.data.tag_name;
|
||||||
const versionWithoutV = version.toLowerCase().replace("v", "");
|
const versionWithoutV = version.toLowerCase().replace("v", "");
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "FETCH_OS_UPDATE_INFO_OK",
|
type: Actions.FETCH_OS_UPDATE_INFO_OK,
|
||||||
payload: versionWithoutV
|
payload: versionWithoutV
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,14 +15,8 @@ describe("<FarmbotOsSettings/>", () => {
|
||||||
auth={fakeState().auth as AuthState} />);
|
auth={fakeState().auth as AuthState} />);
|
||||||
expect(osSettings.find("input").length).toBe(1);
|
expect(osSettings.find("input").length).toBe(1);
|
||||||
expect(osSettings.find("button").length).toBe(9);
|
expect(osSettings.find("button").length).toBe(9);
|
||||||
expect(osSettings.text()).toContain("NAME");
|
["NAME", "TIME ZONE", "LAST SEEN", "FARMBOT OS", "RESTART FARMBOT",
|
||||||
expect(osSettings.text()).toContain("TIME ZONE");
|
"SHUTDOWN FARMBOT", "FACTORY RESET", "CAMERA", "FIRMWARE"].map(string =>
|
||||||
expect(osSettings.text()).toContain("LAST SEEN");
|
expect(osSettings.text()).toContain(string));
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { mount } from "enzyme";
|
||||||
import { HardwareSettings } from "../hardware_settings";
|
import { HardwareSettings } from "../hardware_settings";
|
||||||
import { fakeState } from "../../../__test_support__/fake_state";
|
import { fakeState } from "../../../__test_support__/fake_state";
|
||||||
import { ControlPanelState } from "../../interfaces";
|
import { ControlPanelState } from "../../interfaces";
|
||||||
|
import { Actions } from "../../../constants";
|
||||||
|
|
||||||
describe("<HardwareSettings />", () => {
|
describe("<HardwareSettings />", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -23,9 +24,8 @@ describe("<HardwareSettings />", () => {
|
||||||
controlPanelState={panelState()}
|
controlPanelState={panelState()}
|
||||||
dispatch={jest.fn()}
|
dispatch={jest.fn()}
|
||||||
bot={fakeState().bot} />);
|
bot={fakeState().bot} />);
|
||||||
expect(wrapper.text().toLowerCase()).toContain("expand all");
|
["expand all", "x axis", "motors"].map(string =>
|
||||||
expect(wrapper.text().toLowerCase()).toContain("x axis");
|
expect(wrapper.text().toLowerCase()).toContain(string));
|
||||||
expect(wrapper.text().toLowerCase()).toContain("motors");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function checkDispatch(
|
function checkDispatch(
|
||||||
|
@ -46,14 +46,17 @@ describe("<HardwareSettings />", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
it("expands all", () => {
|
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", () => {
|
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", () => {
|
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()", () => {
|
it("overlapText()", () => {
|
||||||
const spreadData = { active: 100, inactive: 200 };
|
const spreadData = { active: 100, inactive: 200 };
|
||||||
const svgText = shallow(overlapText(100, 100, 150, spreadData));
|
const svgText = shallow(overlapText(100, 100, 150, spreadData));
|
||||||
expect(svgText.text()).toContain("Active: 80%");
|
["Active: 80%", "Inactive: 40%", "orange"].map(string =>
|
||||||
expect(svgText.text()).toContain("Inactive: 40%");
|
expect(svgText.text()).toContain(string));
|
||||||
expect(svgText.text()).toContain("orange");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,10 +21,11 @@ describe("<DragHelperLayer/>", () => {
|
||||||
it("shows drag helpers", () => {
|
it("shows drag helpers", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
const wrapper = shallow(<DragHelperLayer {...p } />);
|
const wrapper = shallow(<DragHelperLayer {...p } />);
|
||||||
expect(wrapper.html()).toContain("drag-helpers");
|
["drag-helpers",
|
||||||
expect(wrapper.html()).toContain("coordinates-tooltip");
|
"coordinates-tooltip",
|
||||||
expect(wrapper.html()).toContain("long-crosshair");
|
"long-crosshair",
|
||||||
expect(wrapper.html()).toContain("short-crosshair");
|
"short-crosshair"].map(string =>
|
||||||
|
expect(wrapper.html()).toContain(string));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't show drag helpers", () => {
|
it("doesn't show drag helpers", () => {
|
||||||
|
|
|
@ -35,13 +35,15 @@ describe("<PlantLayer/>", () => {
|
||||||
const wrapper = shallow(<PlantLayer {...p } />);
|
const wrapper = shallow(<PlantLayer {...p } />);
|
||||||
const layer = wrapper.find("#plant-layer");
|
const layer = wrapper.find("#plant-layer");
|
||||||
expect(layer.find(".plant-link-wrapper").length).toEqual(1);
|
expect(layer.find(".plant-link-wrapper").length).toEqual(1);
|
||||||
expect(layer.html()).toContain("soil-cloud");
|
["soil-cloud",
|
||||||
expect(layer.html()).toContain("plant-icon");
|
"plant-icon",
|
||||||
expect(layer.html()).toContain("image visibility=\"visible\"");
|
"image visibility=\"visible\"",
|
||||||
expect(layer.html()).toContain("/app-resources/img/generic-plant.svg");
|
"/app-resources/img/generic-plant.svg",
|
||||||
expect(layer.html()).toContain("height=\"50\" width=\"50\" x=\"75\" y=\"175\"");
|
"height=\"50\" width=\"50\" x=\"75\" y=\"175\"",
|
||||||
expect(layer.html()).toContain("drag-helpers");
|
"drag-helpers",
|
||||||
expect(layer.html()).toContain("plant-icon");
|
"plant-icon"
|
||||||
|
].map(string =>
|
||||||
|
expect(layer.html()).toContain(string));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles visibility off", () => {
|
it("toggles visibility off", () => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
||||||
import { PlantInventoryItem } from "../plant_inventory_item";
|
import { PlantInventoryItem } from "../plant_inventory_item";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
import { fakePlant } from "../../../__test_support__/fake_state/resources";
|
||||||
|
import { Actions } from "../../../constants";
|
||||||
|
|
||||||
describe("<PlantInventoryItem />", () => {
|
describe("<PlantInventoryItem />", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
@ -24,7 +25,7 @@ describe("<PlantInventoryItem />", () => {
|
||||||
icon: "",
|
icon: "",
|
||||||
plantUUID: "points.1.17"
|
plantUUID: "points.1.17"
|
||||||
},
|
},
|
||||||
type: "TOGGLE_HOVERED_PLANT"
|
type: Actions.TOGGLE_HOVERED_PLANT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ describe("<PlantInventoryItem />", () => {
|
||||||
icon: "",
|
icon: "",
|
||||||
plantUUID: undefined
|
plantUUID: undefined
|
||||||
},
|
},
|
||||||
type: "TOGGLE_HOVERED_PLANT"
|
type: Actions.TOGGLE_HOVERED_PLANT
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,8 +13,12 @@ describe("<PlantInventory />", () => {
|
||||||
<Plants
|
<Plants
|
||||||
plants={[fakePlant()]}
|
plants={[fakePlant()]}
|
||||||
dispatch={jest.fn()} />);
|
dispatch={jest.fn()} />);
|
||||||
expect(wrapper.text()).toContain("DesignerPlantsFarm Events");
|
["Designer",
|
||||||
expect(wrapper.text()).toContain("Strawberry Plant 11 days old");
|
"Plants",
|
||||||
|
"Farm Events",
|
||||||
|
"Strawberry Plant",
|
||||||
|
"11 days old"
|
||||||
|
].map(string => expect(wrapper.text()).toContain(string));
|
||||||
expect(wrapper.find("input").props().placeholder)
|
expect(wrapper.find("input").props().placeholder)
|
||||||
.toEqual("Search your plants...");
|
.toEqual("Search your plants...");
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,9 +38,8 @@ describe("<SelectPlants />", () => {
|
||||||
const p = fakeProps();
|
const p = fakeProps();
|
||||||
p.selected = ["plant.1", "plant.2"];
|
p.selected = ["plant.1", "plant.2"];
|
||||||
const wrapper = mount(<SelectPlants {...p} />);
|
const wrapper = mount(<SelectPlants {...p} />);
|
||||||
expect(wrapper.text()).toContain("Strawberry");
|
["Strawberry", "Blueberry", "Delete"].map(string =>
|
||||||
expect(wrapper.text()).toContain("Blueberry");
|
expect(wrapper.text()).toContain(string));
|
||||||
expect(wrapper.text()).toContain("Delete");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays no selected plants: selection empty", () => {
|
it("displays no selected plants: selection empty", () => {
|
||||||
|
|
|
@ -49,9 +49,8 @@ describe("<FarmwareForms/>", () => {
|
||||||
const wrapper = mount(<FarmwareForms
|
const wrapper = mount(<FarmwareForms
|
||||||
farmwares={fakeFarmwares()}
|
farmwares={fakeFarmwares()}
|
||||||
user_env={{}} />);
|
user_env={{}} />);
|
||||||
expect(wrapper.text()).toContain("My Farmware");
|
["My Farmware", "version: 0.0.0", "Does things."].map(string =>
|
||||||
expect(wrapper.text()).toContain("version: 0.0.0");
|
expect(wrapper.text()).toContain(string));
|
||||||
expect(wrapper.text()).toContain("Does things.");
|
|
||||||
expect(wrapper.find("label").last().text()).toContain("Config 1");
|
expect(wrapper.find("label").last().text()).toContain("Config 1");
|
||||||
expect(wrapper.find("input").props().value).toEqual("4");
|
expect(wrapper.find("input").props().value).toEqual("4");
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,9 +25,11 @@ describe("<FarmwarePage />", () => {
|
||||||
images: []
|
images: []
|
||||||
};
|
};
|
||||||
const wrapper = mount(<FarmwarePage {...props} />);
|
const wrapper = mount(<FarmwarePage {...props} />);
|
||||||
expect(wrapper.text()).toContain("Take Photo");
|
["Take Photo",
|
||||||
expect(wrapper.text()).toContain("Farmware");
|
"Farmware",
|
||||||
expect(wrapper.text()).toContain("Camera Calibration");
|
"Camera Calibration",
|
||||||
expect(wrapper.text()).toContain("Weed Detector");
|
"Weed Detector"
|
||||||
|
].map(string =>
|
||||||
|
expect(wrapper.text()).toContain(string));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,12 +21,14 @@ describe("<CameraCalibration/>", () => {
|
||||||
V_HI: 9
|
V_HI: 9
|
||||||
};
|
};
|
||||||
const wrapper = mount(<CameraCalibration {...props} />);
|
const wrapper = mount(<CameraCalibration {...props} />);
|
||||||
expect(wrapper.text()).toContain("Camera Calibration");
|
["Camera Calibration",
|
||||||
expect(wrapper.text()).toContain("Color Range");
|
"Color Range",
|
||||||
expect(wrapper.text()).toContain("HUE017947");
|
"HUE017947",
|
||||||
expect(wrapper.text()).toContain("SATURATION025558");
|
"SATURATION025558",
|
||||||
expect(wrapper.text()).toContain("VALUE025569");
|
"VALUE025569",
|
||||||
expect(wrapper.text()).toContain("Processing Parameters");
|
"Processing Parameters",
|
||||||
expect(wrapper.text()).toContain("Scan image");
|
"Scan image"
|
||||||
|
].map(string =>
|
||||||
|
expect(wrapper.text()).toContain(string));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,12 +19,14 @@ describe("<WeedDetector />", () => {
|
||||||
images: []
|
images: []
|
||||||
};
|
};
|
||||||
const wrapper = mount(<WeedDetector {...props} />);
|
const wrapper = mount(<WeedDetector {...props} />);
|
||||||
expect(wrapper.text()).toContain("Weed Detector");
|
["Weed Detector",
|
||||||
expect(wrapper.text()).toContain("Color Range");
|
"Color Range",
|
||||||
expect(wrapper.text()).toContain("HUE01793090");
|
"HUE01793090",
|
||||||
expect(wrapper.text()).toContain("SATURATION025550255");
|
"SATURATION025550255",
|
||||||
expect(wrapper.text()).toContain("VALUE025550255");
|
"VALUE025550255",
|
||||||
expect(wrapper.text()).toContain("Processing Parameters");
|
"Processing Parameters",
|
||||||
expect(wrapper.text()).toContain("Scan image");
|
"Scan image"
|
||||||
|
].map(string =>
|
||||||
|
expect(wrapper.text()).toContain(string));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
15
webpack/password_reset/__tests__/on_init_test.ts
Normal file
15
webpack/password_reset/__tests__/on_init_test.ts
Normal file
|
@ -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 "../css/_index.scss";
|
||||||
import { detectLanguage } from "../i18n";
|
import { detectLanguage } from "../i18n";
|
||||||
|
import * as I18n from "i18next";
|
||||||
|
import { onInit } from "./on_init";
|
||||||
|
|
||||||
detectLanguage().then(async (config) => {
|
detectLanguage().then(async config => I18n.init(config, onInit));
|
||||||
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."));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
18
webpack/password_reset/on_init.ts
Normal file
18
webpack/password_reset/on_init.ts
Normal file
|
@ -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 { AuthState } from "./auth/interfaces";
|
||||||
import { HttpData } from "./util";
|
import { HttpData } from "./util";
|
||||||
import { setToken } from "./auth/actions";
|
import { setToken } from "./auth/actions";
|
||||||
import { Session } from "./session";
|
|
||||||
|
|
||||||
type Resp = HttpData<AuthState>;
|
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).
|
/** Grab a new token from the API (won't extend token's exp. date).
|
||||||
* Redirect to home page on failure. */
|
* Redirect to home page on failure. */
|
||||||
export let maybeRefreshToken = (old: AuthState): Promise<AuthState> => {
|
export let maybeRefreshToken
|
||||||
|
= (old: AuthState): Promise<AuthState | undefined> => {
|
||||||
API.setBaseUrl(old.token.unencoded.iss);
|
API.setBaseUrl(old.token.unencoded.iss);
|
||||||
setToken(old); // Precaution: The Axios interceptors might not be set yet.
|
setToken(old); // Precaution: The Axios interceptors might not be set yet.
|
||||||
return axios.get(API.current.tokensPath).then(ok, Session.clear);
|
return axios
|
||||||
};
|
.get(API.current.tokensPath)
|
||||||
|
.then(ok, () => Promise.resolve(undefined));
|
||||||
|
};
|
||||||
|
|
90
webpack/regimens/__tests__/actions_test.ts
Normal file
90
webpack/regimens/__tests__/actions_test.ts
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
32
webpack/regimens/__tests__/index_test.tsx
Normal file
32
webpack/regimens/__tests__/index_test.tsx
Normal file
|
@ -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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
17
webpack/regimens/__tests__/serializers_test.ts
Normal file
17
webpack/regimens/__tests__/serializers_test.ts
Normal file
|
@ -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: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
webpack/regimens/__tests__/state_to_props_test.ts
Normal file
57
webpack/regimens/__tests__/state_to_props_test.ts
Normal file
|
@ -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();
|
const mockErr = jest.fn();
|
||||||
jest.mock("i18next", () => ({ t: (i: string) => i }));
|
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 { 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()", () => {
|
describe("commitBulkEditor()", () => {
|
||||||
it("does nothing if no regimen is selected", () => {
|
beforeEach(() => {
|
||||||
const getState = () => fakeState();
|
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();
|
const dispatch = jest.fn();
|
||||||
commitBulkEditor()(dispatch, getState);
|
commitBulkEditor()(dispatch, getState);
|
||||||
expect(dispatch.mock.calls.length).toEqual(0);
|
expect(dispatch).not.toHaveBeenCalled();
|
||||||
expect(mockErr.mock.calls.length).toEqual(1);
|
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>");
|
||||||
|
});
|
||||||
|
});
|
87
webpack/regimens/bulk_scheduler/__tests__/index_test.tsx
Normal file
87
webpack/regimens/bulk_scheduler/__tests__/index_test.tsx
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
58
webpack/regimens/bulk_scheduler/__tests__/week_grid_test.tsx
Normal file
58
webpack/regimens/bulk_scheduler/__tests__/week_grid_test.tsx
Normal file
|
@ -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.
|
// Proceed only if they selected a sequence from the drop down.
|
||||||
if (selectedSequenceUUID) {
|
if (selectedSequenceUUID) {
|
||||||
const seq = findSequence(res.index, selectedSequenceUUID).body;
|
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.
|
// 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 reg = findRegimen(res.index, currentRegimen);
|
||||||
const update = defensiveClone(reg).body;
|
const update = defensiveClone(reg).body;
|
||||||
update.regimen_items = update.regimen_items.concat(regimenItems);
|
update.regimen_items = update.regimen_items.concat(regimenItems);
|
||||||
|
|
48
webpack/regimens/editor/__tests__/active_editor_test.tsx
Normal file
48
webpack/regimens/editor/__tests__/active_editor_test.tsx
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
84
webpack/regimens/editor/__tests__/index_test.tsx
Normal file
84
webpack/regimens/editor/__tests__/index_test.tsx
Normal file
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
22
webpack/regimens/editor/__tests__/regimen_name_input.tsx
Normal file
22
webpack/regimens/editor/__tests__/regimen_name_input.tsx
Normal file
|
@ -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 { Row, Col } from "../../ui/index";
|
||||||
import { editRegimen } from "../actions";
|
import { editRegimen } from "../actions";
|
||||||
|
|
||||||
function write({ dispatch, regimen }: RegimenProps):
|
export function write({ dispatch, regimen }: RegimenProps):
|
||||||
React.EventHandler<React.FormEvent<{}>> {
|
React.EventHandler<React.FormEvent<{}>> {
|
||||||
if (regimen) {
|
if (regimen) {
|
||||||
return (event: React.FormEvent<HTMLInputElement>) => {
|
return (event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
29
webpack/regimens/list/__tests__/index_test.tsx
Normal file
29
webpack/regimens/list/__tests__/index_test.tsx
Normal file
|
@ -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]} />);
|
sequences={[fakeSequence1, fakeSequence2]} />);
|
||||||
expect(wrapper.find("input").first().props().placeholder)
|
expect(wrapper.find("input").first().props().placeholder)
|
||||||
.toContain("Search Sequences");
|
.toContain("Search Sequences");
|
||||||
expect(wrapper.text()).toContain("Sequence 1");
|
["Sequence 1", "Sequence 2"].map(string =>
|
||||||
expect(wrapper.text()).toContain("Sequence 2");
|
expect(wrapper.text()).toContain(string));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function fetchSyncData(dispatch: Function) {
|
||||||
type, payload: { name, data: r.data }
|
type, payload: { name, data: r.data }
|
||||||
}), fail);
|
}), fail);
|
||||||
|
|
||||||
const fail = () => warning("Please try refreshing the page.",
|
const fail = () => warning("Please try refreshing the page or logging in again.",
|
||||||
"Error downloading data");
|
"Error downloading data");
|
||||||
|
|
||||||
fetch<User>("users", API.current.usersPath);
|
fetch<User>("users", API.current.usersPath);
|
||||||
|
@ -47,16 +47,3 @@ export function fetchSyncData(dispatch: Function) {
|
||||||
fetch<Sequence[]>("sequences", API.current.sequencesPath);
|
fetch<Sequence[]>("sequences", API.current.sequencesPath);
|
||||||
fetch<Tool[]>("tools", API.current.toolsPath);
|
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: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
39
webpack/tools/__tests__/index_test.tsx
Normal file
39
webpack/tools/__tests__/index_test.tsx
Normal file
|
@ -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>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{tools.map((tool: TaggedTool) => {
|
{tools.map((tool: TaggedTool) => {
|
||||||
return <Row key={tool.body.id}>
|
return <Row key={tool.uuid}>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
{tool.body.name || "Name not found"}
|
{tool.body.name || "Name not found"}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class ToolBayList extends React.Component<ToolBayListProps, {}> {
|
||||||
{getToolSlots().map((slot: TaggedToolSlotPointer, index: number) => {
|
{getToolSlots().map((slot: TaggedToolSlotPointer, index: number) => {
|
||||||
const tool = getToolByToolSlotUUID(slot.uuid);
|
const tool = getToolByToolSlotUUID(slot.uuid);
|
||||||
const name = (tool && tool.body.name) || "None";
|
const name = (tool && tool.body.name) || "None";
|
||||||
return <Row key={slot.body.id}>
|
return <Row key={slot.uuid}>
|
||||||
<Col xs={2}>
|
<Col xs={2}>
|
||||||
<label>{index + 1}</label>
|
<label>{index + 1}</label>
|
||||||
</Col>
|
</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
|
// 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 { getParam, HttpData } from "./util";
|
||||||
import axios from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { API } from "./api/api";
|
import { API } from "./api/api";
|
||||||
import { Session } from "./session";
|
import { Session } from "./session";
|
||||||
import { AuthState } from "./auth/interfaces";
|
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 =
|
export const FAILURE_PAGE =
|
||||||
`<p>
|
`<p>
|
||||||
We were unable to verify your account.
|
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.
|
/** Function called when the Frontend verifies its registration token.
|
||||||
* IF YOU BREAK THIS FUNCTION, YOU BREAK *ALL* NEW USER REGISTRATIONS. */
|
* 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() {
|
export async function attempt() {
|
||||||
API.setBaseUrl(API.fetchBrowserLocation());
|
API.setBaseUrl(API.fetchBrowserLocation());
|
||||||
|
@ -28,7 +47,23 @@ export async function attempt() {
|
||||||
window.location.href = API.current.baseUrl + "/app/controls";
|
window.location.href = API.current.baseUrl + "/app/controls";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fail() {
|
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);
|
document.write(FAILURE_PAGE);
|
||||||
throw new Error(FAILURE_MSG);
|
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"
|
farmbot-toastr "^1.0.0"
|
||||||
typescript "^2.3.4"
|
typescript "^2.3.4"
|
||||||
|
|
||||||
farmbot@5.0.1-rc12:
|
farmbot@5.0.1-rc13:
|
||||||
version "5.0.1-rc12"
|
version "5.0.1-rc13"
|
||||||
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc12.tgz#3746018e2d42657ece67ff2af23b507d604ceb0d"
|
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.0.1-rc13.tgz#473366b179eb967d9f1265952334b6195de7b1ef"
|
||||||
dependencies:
|
dependencies:
|
||||||
mqtt "^1.7.4"
|
mqtt "^1.7.4"
|
||||||
typescript "^2.4.2"
|
typescript "^2.4.2"
|
||||||
|
|
Loading…
Reference in a new issue