From 663250227d8c63869780c116117738b812cfced8 Mon Sep 17 00:00:00 2001 From: Andrew Cantino Date: Sat, 31 May 2014 23:36:33 -0700 Subject: [PATCH] allow exporting of a set of Agents with their links from a Scenario; Scenario guid is now generated and copied to export, as well as a source link when public --- {lib => app/concerns}/assignable_types.rb | 0 {lib => app/concerns}/inheritance_tracking.rb | 0 .../concerns}/json_serialized_field.rb | 0 .../concerns}/markdown_class_attributes.rb | 0 app/controllers/scenarios_controller.rb | 17 ++- app/models/scenario.rb | 8 +- app/views/scenarios/_form.html.erb | 19 ++- app/views/scenarios/index.html.erb | 6 +- app/views/scenarios/share.html.erb | 16 ++ app/views/scenarios/show.html.erb | 13 +- config/routes.rb | 1 + .../20140531232016_add_fields_to_scenarios.rb | 8 + db/schema.rb | 142 +++++++++--------- lib/agents_exporter.rb | 53 +++++++ .../inheritance_tracking_spec.rb | 0 spec/controllers/scenarios_controller_spec.rb | 56 ++++++- spec/fixtures/scenarios.yml | 6 + spec/lib/agents_exporter_spec.rb | 58 +++++++ spec/models/scenario_spec.rb | 11 ++ 19 files changed, 331 insertions(+), 83 deletions(-) rename {lib => app/concerns}/assignable_types.rb (100%) rename {lib => app/concerns}/inheritance_tracking.rb (100%) rename {lib => app/concerns}/json_serialized_field.rb (100%) rename {lib => app/concerns}/markdown_class_attributes.rb (100%) create mode 100644 db/migrate/20140531232016_add_fields_to_scenarios.rb create mode 100644 lib/agents_exporter.rb rename spec/{lib => concerns}/inheritance_tracking_spec.rb (100%) create mode 100644 spec/lib/agents_exporter_spec.rb diff --git a/lib/assignable_types.rb b/app/concerns/assignable_types.rb similarity index 100% rename from lib/assignable_types.rb rename to app/concerns/assignable_types.rb diff --git a/lib/inheritance_tracking.rb b/app/concerns/inheritance_tracking.rb similarity index 100% rename from lib/inheritance_tracking.rb rename to app/concerns/inheritance_tracking.rb diff --git a/lib/json_serialized_field.rb b/app/concerns/json_serialized_field.rb similarity index 100% rename from lib/json_serialized_field.rb rename to app/concerns/json_serialized_field.rb diff --git a/lib/markdown_class_attributes.rb b/app/concerns/markdown_class_attributes.rb similarity index 100% rename from lib/markdown_class_attributes.rb rename to app/concerns/markdown_class_attributes.rb diff --git a/app/controllers/scenarios_controller.rb b/app/controllers/scenarios_controller.rb index 55724018..7ad64310 100644 --- a/app/controllers/scenarios_controller.rb +++ b/app/controllers/scenarios_controller.rb @@ -1,4 +1,6 @@ class ScenariosController < ApplicationController + skip_before_filter :authenticate_user!, :only => :export + def index @scenarios = current_user.scenarios.page(params[:page]) @@ -27,10 +29,8 @@ class ScenariosController < ApplicationController end end - # Share is a work in progress! def share @scenario = current_user.scenarios.find(params[:id]) - @agents = @scenario.agents.preload(:scenarios).page(params[:page]) respond_to do |format| format.html @@ -38,6 +38,19 @@ class ScenariosController < ApplicationController end end + def export + @scenario = Scenario.find(params[:id]) + raise ActiveRecord::RecordNotFound unless @scenario.public? || (current_user && current_user.id == @scenario.user_id) + + @exporter = AgentsExporter.new(:name => @scenario.name, + :description => @scenario.description, + :guid => @scenario.guid, + :source_url => @scenario.public? && export_scenario_url(@scenario), + :agents => @scenario.agents) + response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"' + render :json => JSON.pretty_generate(@exporter.as_json) + end + def edit @scenario = current_user.scenarios.find(params[:id]) diff --git a/app/models/scenario.rb b/app/models/scenario.rb index e6907098..d3010e97 100644 --- a/app/models/scenario.rb +++ b/app/models/scenario.rb @@ -1,16 +1,22 @@ class Scenario < ActiveRecord::Base - attr_accessible :name, :agent_ids + attr_accessible :name, :agent_ids, :description, :public belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios + before_save :make_guid + validates_presence_of :name, :user validate :agents_are_owned protected + def make_guid + self.guid = SecureRandom.hex unless guid.present? + end + def agents_are_owned errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } end diff --git a/app/views/scenarios/_form.html.erb b/app/views/scenarios/_form.html.erb index 6b21e07d..0586aa93 100644 --- a/app/views/scenarios/_form.html.erb +++ b/app/views/scenarios/_form.html.erb @@ -12,11 +12,28 @@
<%= f.label :name %> - <%= f.text_field :name, :class => 'form-control' %> + <%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %>
+
+
+
+ <%= f.label :description, "Optional Description" %> + <%= f.text_area :description, :rows => 10, :class => 'form-control', :placeholder => "Optionally describe what this set of Agents will do" %> +
+ +
+ <%= f.label :public do %> + <%= f.check_box :public %> Share this Scenario publicly + <% end %> + +
+ +
+
+
diff --git a/app/views/scenarios/index.html.erb b/app/views/scenarios/index.html.erb index 36adf24c..03e621f7 100644 --- a/app/views/scenarios/index.html.erb +++ b/app/views/scenarios/index.html.erb @@ -20,14 +20,16 @@ <% @scenarios.each do |scenario| %> - <%= scenario.name %> + + <%= link_to(scenario.name, scenario, class: "label label-info") %> + <%= link_to pluralize(scenario.agents.count, "agent"), scenario %>
<%= link_to 'Show', scenario, class: "btn btn-default" %> <%= link_to 'Edit', edit_scenario_path(scenario), class: "btn btn-default" %> <%= link_to 'Share', share_scenario_path(scenario), class: "btn btn-default" %> - <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-default" %> + <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: { confirm: "This will remove the '#{scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %>
diff --git a/app/views/scenarios/share.html.erb b/app/views/scenarios/share.html.erb index c5e6c895..85b32dae 100644 --- a/app/views/scenarios/share.html.erb +++ b/app/views/scenarios/share.html.erb @@ -5,6 +5,22 @@

Share <%= @scenario.name %> with the world

+

+ Please be sure that none of the Agents in this Scenario have sensitive data in their settings before sharing! +

+ + <% if @scenario.public? %> +

+ This Scenario is public. You can <%= link_to "download and share your export file", export_scenario_path(@scenario) %>, or give out this URL: +

+ +
+ +
+ <% else %> + This Scenario is not public. You can share it by <%= link_to "downloading and sharing your export file", export_scenario_path(@scenario) %>. + <% end %> +
diff --git a/app/views/scenarios/show.html.erb b/app/views/scenarios/show.html.erb index 190e47d3..ba392b5f 100644 --- a/app/views/scenarios/show.html.erb +++ b/app/views/scenarios/show.html.erb @@ -2,20 +2,19 @@
+ <%= render 'agents/table', :returnTo => scenario_path(@scenario) %> + +
+
<%= link_to ' Back'.html_safe, scenarios_path, class: "btn btn-default" %> <%= link_to ' Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> <%= link_to ' Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %> + <%= link_to ' Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %>
- - - - <%= render 'agents/table', :returnTo => scenario_path(@scenario) %>
diff --git a/config/routes.rb b/config/routes.rb index b5b676ea..bfefbad9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ Huginn::Application.routes.draw do resources :scenarios do member do get :share + get :export end end diff --git a/db/migrate/20140531232016_add_fields_to_scenarios.rb b/db/migrate/20140531232016_add_fields_to_scenarios.rb new file mode 100644 index 00000000..0a8861b2 --- /dev/null +++ b/db/migrate/20140531232016_add_fields_to_scenarios.rb @@ -0,0 +1,8 @@ +class AddFieldsToScenarios < ActiveRecord::Migration + def change + add_column :scenarios, :description, :text + add_column :scenarios, :public, :boolean, :default => false, :null => false + add_column :scenarios, :guid, :string, :null => false + add_column :scenarios, :source_url, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c326fd5b..3a990c81 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -9,23 +9,23 @@ # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140408150825) do +ActiveRecord::Schema.define(version: 20140531232016) do - create_table "agent_logs", :force => true do |t| - t.integer "agent_id", :null => false - t.text "message", :null => false - t.integer "level", :default => 3, :null => false + create_table "agent_logs", force: true do |t| + t.integer "agent_id", null: false + t.text "message", limit: 16777215, null: false + t.integer "level", default: 3, null: false t.integer "inbound_event_id" t.integer "outbound_event_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table "agents", :force => true do |t| + create_table "agents", force: true do |t| t.integer "user_id" - t.text "options" + t.text "options", limit: 16777215 t.string "type" t.string "name" t.string "schedule" @@ -33,73 +33,62 @@ ActiveRecord::Schema.define(:version => 20140408150825) do t.datetime "last_check_at" t.datetime "last_receive_at" t.integer "last_checked_event_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.text "memory", :limit => 2147483647 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "memory", limit: 2147483647 t.datetime "last_web_request_at" - t.integer "keep_events_for", :default => 0, :null => false t.datetime "last_event_at" t.datetime "last_error_log_at" - t.boolean "propagate_immediately", :default => false, :null => false - t.boolean "disabled", :default => false, :null => false + t.integer "keep_events_for", default: 0, null: false + t.boolean "propagate_immediately", default: false, null: false + t.boolean "disabled", default: false, null: false end - add_index "agents", ["schedule"], :name => "index_agents_on_schedule" - add_index "agents", ["type"], :name => "index_agents_on_type" - add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at" + add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree + add_index "agents", ["type"], name: "index_agents_on_type", using: :btree + add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree - create_table "delayed_jobs", :force => true do |t| - t.integer "priority", :default => 0 - t.integer "attempts", :default => 0 - t.text "handler", :limit => 16777215 - t.text "last_error" + create_table "delayed_jobs", force: true do |t| + t.integer "priority", default: 0 + t.integer "attempts", default: 0 + t.text "handler", limit: 16777215 + t.text "last_error", limit: 16777215 t.datetime "run_at" t.datetime "locked_at" t.datetime "failed_at" t.string "locked_by" t.string "queue" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree - create_table "events", :force => true do |t| + create_table "events", force: true do |t| t.integer "user_id" t.integer "agent_id" - t.decimal "lat", :precision => 15, :scale => 10 - t.decimal "lng", :precision => 15, :scale => 10 - t.text "payload", :limit => 16777215 - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.decimal "lat", precision: 15, scale: 10 + t.decimal "lng", precision: 15, scale: 10 + t.text "payload", limit: 2147483647 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.datetime "expires_at" end - add_index "events", ["agent_id", "created_at"], :name => "index_events_on_agent_id_and_created_at" - add_index "events", ["expires_at"], :name => "index_events_on_expires_at" - add_index "events", ["user_id", "created_at"], :name => "index_events_on_user_id_and_created_at" + add_index "events", ["agent_id", "created_at"], name: "index_events_on_agent_id_and_created_at", using: :btree + add_index "events", ["expires_at"], name: "index_events_on_expires_at", using: :btree + add_index "events", ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at", using: :btree - create_table "links", :force => true do |t| + create_table "links", force: true do |t| t.integer "source_id" t.integer "receiver_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.integer "event_id_at_creation", :default => 0, :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "event_id_at_creation", default: 0, null: false end - add_index "links", ["receiver_id", "source_id"], :name => "index_links_on_receiver_id_and_source_id" - add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id" - - create_table "user_credentials", :force => true do |t| - t.integer "user_id", :null => false - t.string "credential_name", :null => false - t.text "credential_value", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.string "mode", :default => "text", :null => false - end - - add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true + add_index "links", ["receiver_id", "source_id"], name: "index_links_on_receiver_id_and_source_id", using: :btree + add_index "links", ["source_id", "receiver_id"], name: "index_links_on_source_id_and_receiver_id", using: :btree create_table "scenario_memberships", force: true do |t| t.integer "agent_id", null: false @@ -109,37 +98,52 @@ ActiveRecord::Schema.define(:version => 20140408150825) do end create_table "scenarios", force: true do |t| - t.string "name", null: false - t.integer "user_id", null: false + t.string "name", null: false + t.integer "user_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.text "description" + t.boolean "public", default: false, null: false + t.string "guid", null: false + t.string "source_url" end - create_table "users", :force => true do |t| - t.string "email", :default => "", :null => false - t.string "encrypted_password", :default => "", :null => false + create_table "user_credentials", force: true do |t| + t.integer "user_id", null: false + t.string "credential_name", null: false + t.text "credential_value", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "mode", default: "text", null: false + end + + add_index "user_credentials", ["user_id", "credential_name"], name: "index_user_credentials_on_user_id_and_credential_name", unique: true, using: :btree + + create_table "users", force: true do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", :default => 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.boolean "admin", :default => false, :null => false - t.integer "failed_attempts", :default => 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false, null: false + t.integer "failed_attempts", default: 0 t.string "unlock_token" t.datetime "locked_at" - t.string "username", :null => false - t.string "invitation_code", :null => false + t.string "username", null: false + t.string "invitation_code", null: false t.integer "scenario_count", default: 0, null: false end - add_index "users", ["email"], :name => "index_users_on_email", :unique => true - add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true - add_index "users", ["unlock_token"], :name => "index_users_on_unlock_token", :unique => true - add_index "users", ["username"], :name => "index_users_on_username", :unique => true + add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + add_index "users", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree + add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree end diff --git a/lib/agents_exporter.rb b/lib/agents_exporter.rb new file mode 100644 index 00000000..1826cccc --- /dev/null +++ b/lib/agents_exporter.rb @@ -0,0 +1,53 @@ +class AgentsExporter + attr_accessor :options + + def initialize(options) + self.options = options + end + + # Filename should have no commas or special characters to support Content-Disposition on older browsers. + def filename + ((options[:name] || '').downcase.gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-|-$/, '').presence || 'exported-agents') + ".json" + end + + def as_json(opts = {}) + { + :name => options[:name].presence || 'No name provided', + :description => options[:description].presence || 'No description provided', + :source_url => options[:source_url], + :guid => options[:guid], + :exported_at => Time.now.utc.iso8601, + :agents => agents.map { |agent| agent_as_json(agent) }, + :links => links + } + end + + def agents + options[:agents].to_a + end + + def links + agent_ids = agents.map(&:id) + + contained_links = agents.map.with_index do |agent, index| + agent.links_as_source.where(:receiver_id => agent_ids).map do |link| + { :source => index, :receiver => agent_ids.index(link.receiver_id) } + end + end + + contained_links.flatten.compact + end + + def agent_as_json(agent) + { + :type => agent.type, + :name => agent.name, + :schedule => agent.schedule, + :keep_events_for => agent.keep_events_for, + :propagate_immediately => agent.propagate_immediately, + :disabled => agent.disabled, + :source_system_agent_id => agent.id, + :options => agent.options + } + end +end \ No newline at end of file diff --git a/spec/lib/inheritance_tracking_spec.rb b/spec/concerns/inheritance_tracking_spec.rb similarity index 100% rename from spec/lib/inheritance_tracking_spec.rb rename to spec/concerns/inheritance_tracking_spec.rb diff --git a/spec/controllers/scenarios_controller_spec.rb b/spec/controllers/scenarios_controller_spec.rb index b976c04c..9b122fee 100644 --- a/spec/controllers/scenarios_controller_spec.rb +++ b/spec/controllers/scenarios_controller_spec.rb @@ -32,6 +32,59 @@ describe ScenariosController do end end + describe "GET share" do + it "only displays Scenario share information for the current user" do + get :share, :id => scenarios(:bob_weather).to_param + assigns(:scenario).should eq(scenarios(:bob_weather)) + + lambda { + get :share, :id => scenarios(:jane_weather).to_param + }.should raise_error(ActiveRecord::RecordNotFound) + end + end + + describe "GET export" do + it "returns a JSON file download from an instantiated AgentsExporter" do + get :export, :id => scenarios(:bob_weather).to_param + assigns(:exporter).options[:name].should == scenarios(:bob_weather).name + assigns(:exporter).options[:description].should == scenarios(:bob_weather).description + assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents + assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid + assigns(:exporter).options[:source_url].should be_false + response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"' + response.headers['Content-Type'].should == 'application/json; charset=utf-8' + JSON.parse(response.body)["name"].should == scenarios(:bob_weather).name + end + + it "only exports private Scenarios for the current user" do + get :export, :id => scenarios(:bob_weather).to_param + assigns(:scenario).should eq(scenarios(:bob_weather)) + + lambda { + get :export, :id => scenarios(:jane_weather).to_param + }.should raise_error(ActiveRecord::RecordNotFound) + end + + describe "public exports" do + before do + scenarios(:jane_weather).update_attribute :public, true + end + + it "exports public scenarios for other users when logged in" do + get :export, :id => scenarios(:jane_weather).to_param + assigns(:scenario).should eq(scenarios(:jane_weather)) + assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) + end + + it "exports public scenarios for other users when logged out" do + sign_out :user + get :export, :id => scenarios(:jane_weather).to_param + assigns(:scenario).should eq(scenarios(:jane_weather)) + assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) + end + end + end + describe "GET edit" do it "only shows Scenarios for the current user" do get :edit, :id => scenarios(:bob_weather).to_param @@ -67,9 +120,10 @@ describe ScenariosController do describe "PUT update" do it "updates attributes on Scenarios for the current user" do - post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name" } + post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" } response.should redirect_to(scenario_path(scenarios(:bob_weather))) scenarios(:bob_weather).reload.name.should == "new_name" + scenarios(:bob_weather).should be_public lambda { post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" } diff --git a/spec/fixtures/scenarios.yml b/spec/fixtures/scenarios.yml index b7b9249a..07a2b2d4 100644 --- a/spec/fixtures/scenarios.yml +++ b/spec/fixtures/scenarios.yml @@ -1,7 +1,13 @@ jane_weather: name: Jane's weather alert Scenario user: jane + description: Jane's weather alert system + public: false + guid: random-guid-generated-by-bob bob_weather: name: Bob's weather alert Scenario user: bob + description: Bob's weather alert system + public: false + guid: random-guid-generated-by-jane diff --git a/spec/lib/agents_exporter_spec.rb b/spec/lib/agents_exporter_spec.rb new file mode 100644 index 00000000..b028d2d3 --- /dev/null +++ b/spec/lib/agents_exporter_spec.rb @@ -0,0 +1,58 @@ +# encoding: utf-8 + +require 'spec_helper' + +describe AgentsExporter do + describe "#as_json" do + let(:name) { "My set of Agents" } + let(:description) { "These Agents work together nicely!" } + let(:guid) { "some-guid" } + let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" } + let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] } + let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) } + + it "outputs a structure containing name, description, the date, all agents & their links" do + data = exporter.as_json + data[:name].should == name + data[:description].should == description + data[:source_url].should == source_url + data[:guid].should == guid + Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) + data[:links].should == [{ :source => 0, :receiver => 1 }] + data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } + data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true + end + + it "does not output links to other agents" do + Link.create!(:source_id => agents(:jane_weather_agent).id, :receiver_id => agents(:jane_website_agent).id) + Link.create!(:source_id => agents(:jane_website_agent).id, :receiver_id => agents(:jane_rain_notifier_agent).id) + + exporter.as_json[:links].should == [{ :source => 0, :receiver => 1 }] + end + end + + describe "#filename" do + it "strips special characters" do + AgentsExporter.new(:name => "ƏfooƐƕƺbar").filename.should == "foo-bar.json" + end + + it "strips punctuation" do + AgentsExporter.new(:name => "foo,bar").filename.should == "foo-bar.json" + end + + it "strips leading and trailing dashes" do + AgentsExporter.new(:name => ",foo,").filename.should == "foo.json" + end + + it "has a default when options[:name] is nil" do + AgentsExporter.new(:name => nil).filename.should == "exported-agents.json" + end + + it "has a default when the result is empty" do + AgentsExporter.new(:name => "").filename.should == "exported-agents.json" + AgentsExporter.new(:name => "Ə").filename.should == "exported-agents.json" + AgentsExporter.new(:name => "-").filename.should == "exported-agents.json" + AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json" + end + end +end \ No newline at end of file diff --git a/spec/models/scenario_spec.rb b/spec/models/scenario_spec.rb index f3172f7c..8510b0c0 100644 --- a/spec/models/scenario_spec.rb +++ b/spec/models/scenario_spec.rb @@ -26,6 +26,17 @@ describe Scenario do end end + describe "guid" do + it "gets created before_save, but only if it's not present" do + scenario = users(:bob).scenarios.new(:name => "some scenario") + scenario.guid.should be_nil + scenario.save! + scenario.guid.should_not be_nil + + lambda { scenario.save! }.should_not change { scenario.reload.guid } + end + end + describe "counters" do before do @scenario = users(:bob).scenarios.new(:name => "some scenario")