diff --git a/app/lib/presenters/component_presenter.rb b/app/lib/presenters/component_presenter.rb index 00a81a12..29003c77 100644 --- a/app/lib/presenters/component_presenter.rb +++ b/app/lib/presenters/component_presenter.rb @@ -8,7 +8,7 @@ def default_options end def exposed_fields - %i{key sensor last_reading_at latest_value previous_value readings} + %i{sensor last_reading_at latest_value previous_value readings} end def sensor diff --git a/app/lib/presenters/device_presenter.rb b/app/lib/presenters/device_presenter.rb index 7b6584aa..27565b53 100644 --- a/app/lib/presenters/device_presenter.rb +++ b/app/lib/presenters/device_presenter.rb @@ -60,7 +60,7 @@ def hardware def owner if options[:with_owner] && device.owner - present(device.owner, with_devices: false) + return present(device.owner, with_devices: false) end end @@ -77,7 +77,7 @@ def mac_address end def components - present(device.components) + present(device.components, readings: options[:readings]) end private diff --git a/app/lib/presenters/sensor_presenter.rb b/app/lib/presenters/sensor_presenter.rb index da50c42a..0d355b80 100644 --- a/app/lib/presenters/sensor_presenter.rb +++ b/app/lib/presenters/sensor_presenter.rb @@ -4,7 +4,7 @@ class SensorPresenter < BasePresenter alias_method :sensor, :model def exposed_fields - %i{id parent_id name description unit created_at updated_at uuid default_key datasheet unit_definition measurement tags} + %i{id parent_id name description unit created_at updated_at uuid datasheet unit_definition measurement tags} end def measurement diff --git a/spec/presenters/component_presenter_spec.rb b/spec/presenters/component_presenter_spec.rb new file mode 100644 index 00000000..924032a0 --- /dev/null +++ b/spec/presenters/component_presenter_spec.rb @@ -0,0 +1,95 @@ +require "rails_helper" +describe Presenters::ComponentPresenter do + + let(:sensor_presentation) { + double(:sensor_presentation) + } + + let(:component) { + FactoryBot.create(:component) + } + + let(:current_user) { + FactoryBot.create(:user) + } + + let(:render_context) { + double(:render_context) + } + + let(:options) { + {} + } + + subject(:presenter) { Presenters::ComponentPresenter.new(component, current_user, render_context, options) } + + it "exposes the last_reading_at date" do + expect(presenter.as_json[:last_reading_at]).to eq(component.last_reading_at) + end + + it "exposes a presentation of the sensor" do + allow(Presenters).to receive(:present).with(component.sensor, current_user, render_context, {}).and_return(sensor_presentation) + expect(presenter.as_json[:sensor]).to eq(sensor_presentation) + end + + context "when the component's device has data" do + it "returns the data for the corresponding sensor as the latest_value" do + allow(component.device).to receive(:data).and_return({ + component.sensor_id.to_s => 123.0 + }) + expect(presenter.as_json[:latest_value]).to eq(123.0) + end + end + + context "when the component's device has no data" do + it "has no latest_value" do + expect(presenter.as_json[:latest_value]).to eq(nil) + end + end + + context "when the component's device has old_data" do + it "returns the old data for the corresponding sensor as the previous_value" do + allow(component.device).to receive(:old_data).and_return({ + component.sensor_id.to_s => 246.0 + }) + expect(presenter.as_json[:previous_value]).to eq(246.0) + end + end + + context "when the component's device has no old_data" do + it "has no previous_value" do + expect(presenter.as_json[:previous_value]).to eq(nil) + end + end + + context "when readings are supplied" do + let(:reading_timestamp) { Time.now - 6.hours } + let(:reading_value) { 1234.1 } + let(:reading_raw_value) { 2468.1 } + let(:options) { + { + readings: [ + #TODO this particular reading format needs to be refactored out + { + "" => reading_timestamp, + "#{component.sensor_id}" => reading_value, + "#{component.sensor_id}_raw" => reading_raw_value, + "#{component.sensor_id + 1}" => 54321.0 + } + ] + } + } + + it "returns the readings formatted with timestamp, value and raw_value" do + expect(presenter.as_json[:readings]).to eq([ + { timestamp: reading_timestamp, value: reading_value, raw_value: reading_raw_value } + ]) + end + end + + context "when readings are not supplied" do + it "has no readings" do + expect(presenter.as_json[:readings]).to eq(nil) + end + end +end diff --git a/spec/presenters/device_presenter_spec.rb b/spec/presenters/device_presenter_spec.rb new file mode 100644 index 00000000..2a89ebc6 --- /dev/null +++ b/spec/presenters/device_presenter_spec.rb @@ -0,0 +1,267 @@ +require "rails_helper" +describe Presenters::DevicePresenter do + + let(:owner) { + FactoryBot.create(:user) + } + + let(:components) { + double(:components) + } + + let(:device) { + FactoryBot.create(:device, owner: owner).tap do |device| + allow(device).to receive(:components).and_return(components) + end + } + + let(:current_user) { + FactoryBot.create(:user) + } + + let(:render_context) { + double(:render_context) + } + + let(:options) { + { readings: readings } + } + + let(:readings) { + double(:readings) + } + + let(:owner_presentation) { + double(:owner_presentation) + } + + let(:components_presentation) { + double(:components_presentation) + } + + let(:show_private_info) { false } + + let(:device_policy) { + double(:device_policy).tap do |device_policy| + allow(device_policy).to receive(:show_private_info?).and_return(show_private_info) + end + } + + + before do + allow(DevicePolicy).to receive(:new).and_return(device_policy) + + allow(Presenters).to receive(:present) do |model| + case model + when owner + owner_presentation + when components + components_presentation + end + end + end + + subject(:presenter) { Presenters::DevicePresenter.new(device, current_user, render_context, options) } + + it "exposes the id" do + expect(presenter.as_json[:id]).to eq(device.id) + end + + it "exposes the uuid" do + expect(presenter.as_json[:uuid]).to eq(device.uuid) + end + + it "exposes the name" do + expect(presenter.as_json[:name]).to eq(device.name) + end + + it "exposes the description" do + expect(presenter.as_json[:description]).to eq(device.description) + end + + it "exposes the state" do + expect(presenter.as_json[:state]).to eq(device.state) + end + + it "exposes the system_tags" do + expect(presenter.as_json[:system_tags]).to eq(device.system_tags) + end + + it "exposes the user_tags" do + expect(presenter.as_json[:user_tags]).to eq(device.user_tags) + end + + it "exposes the last_reading_at date" do + expect(presenter.as_json[:last_reading_at]).to eq(device.last_reading_at) + end + + it "exposes the created_at date" do + expect(presenter.as_json[:created_at]).to eq(device.created_at) + end + + it "exposes the updated_at date" do + expect(presenter.as_json[:updated_at]).to eq(device.updated_at) + end + + it "includes the notification statuses" do + expect(presenter.as_json[:notify][:stopped_publishing]).to eq(device.notify_stopped_publishing) + expect(presenter.as_json[:notify][:low_battery]).to eq(device.notify_low_battery) + end + + it "includes public hardware info" do + expect(presenter.as_json[:hardware][:name]).to eq(device.hardware_name) + expect(presenter.as_json[:hardware][:type]).to eq(device.hardware_type) + expect(presenter.as_json[:hardware][:version]).to eq(device.hardware_version) + expect(presenter.as_json[:hardware][:slug]).to eq(device.hardware_slug) + end + + context "by default" do + it "includes the location information" do + expect(presenter.as_json[:location][:exposure]).to eq(device.exposure) + expect(presenter.as_json[:location][:elevation]).to eq(device.elevation&.to_i) + expect(presenter.as_json[:location][:latitude]).to eq(device.latitude) + expect(presenter.as_json[:location][:longitude]).to eq(device.longitude) + expect(presenter.as_json[:location][:geohash]).to eq(device.geohash) + expect(presenter.as_json[:location][:city]).to eq(device.city) + expect(presenter.as_json[:location][:country]).to eq(device.country_name) + end + + it "includes the postprocessing information" do + expect(presenter.as_json[:postprocessing]).to eq(device.postprocessing) + end + + it "includes a presentation of the owner, without associated devices" do + expect(presenter.as_json[:owner]).to eq(owner_presentation) + expect(Presenters).to have_received(:present).with(device.owner, current_user, render_context, with_devices: false) + end + + it "includes a presentation of the components, passing the readings as an option" do + expect(presenter.as_json[:components]).to eq(components_presentation) + expect(Presenters).to have_received(:present).with(device.components, current_user, render_context, readings: readings) + end + end + + context "when with_location is false" do + let(:options) { { with_location: false } } + it "does not include the location information" do + expect(presenter.as_json[:location]).to be(nil) + end + end + + + context "when with_postprocessing is false" do + let(:options) { { with_postprocessing: false } } + it "does not include the postprocessing information" do + expect(presenter.as_json[:postprocessing]).to be(nil) + end + end + + context "when with_owner is false" do + let(:options) { { with_owner: false } } + it "does not include the owner" do + expect(presenter.as_json[:owner]).to be(nil) + expect(Presenters).not_to have_received(:present).with(device.owner, current_user, render_context, with_devices: false) + end + end + + context "when the user is authorized to view the device's private info" do + + let(:show_private_info) { true } + context "when the never_authorized option is true" do + + let(:options) { { never_authorized: true } } + + it "does not include the hardware status message" do + expect(presenter.as_json[:hardware][:last_status_message]).to be(nil) + end + + it "includes hardware status_message in the hardware unauthorized_fields" do + expect(presenter.as_json[:hardware][:unauthorized_fields]).to include(:last_status_message) + end + + it "does not include the data_policy" do + expect(presenter.as_json[:data_policy]).to be(nil) + end + + it "includes the data_policy in the unauthorized_fields" do + expect(presenter.as_json[:unauthorized_fields]).to include(:data_policy) + end + + it "does not include the device_token" do + expect(presenter.as_json[:device_token]).to be(nil) + end + + it "includes the device_token in the unauthorized_fields" do + expect(presenter.as_json[:unauthorized_fields]).to include(:device_token) + end + + it "does not include the mac_address" do + expect(presenter.as_json[:mac_address]).to be(nil) + end + + it "includes the mac_address in the unauthorized_fields" do + expect(presenter.as_json[:unauthorized_fields]).to include(:mac_address) + end + end + + + context "when the never_authorized option is false" do + let(:options) { { never_authorized: false } } + + it "includes the hardware status message" do + expect(presenter.as_json[:hardware][:last_status_message]).to eq(device.hardware_info) + end + + it "does not include hardware unauthorized_fields" do + expect(presenter.as_json[:hardware]).not_to include(:unauthorized_fields) + end + + it "includes the data_policy" do + expect(presenter.as_json[:data_policy][:is_private]).to eq(device.is_private) + expect(presenter.as_json[:data_policy][:enable_forwarding]).to eq(device.enable_forwarding) + expect(presenter.as_json[:data_policy][:precise_location]).to eq(device.precise_location) + end + + it "includes the device_token" do + expect(presenter.as_json[:device_token]).to eq(device.device_token) + end + + it "includes the mac_address" do + expect(presenter.as_json[:mac_address]).to eq(device.mac_address) + end + + it "does not include unauthorized_fields" do + expect(presenter.as_json).not_to include(:unauthorized_fields) + end + end + end + + context "when the user is not authorized to view the device's private info" do + let(:show_private_info) { false } + it "does not include the hardware status message" do + expect(presenter.as_json[:hardware][:last_status_message]).to be(nil) + end + + it "includes hardware status_message in the hardware unauthorized_fields" do + expect(presenter.as_json[:hardware][:unauthorized_fields]).to include(:last_status_message) + end + + it "does not include the data_policy" do + expect(presenter.as_json[:data_policy]).to be(nil) + end + + it "includes the data_policy in the unauthorized_fields" do + expect(presenter.as_json[:unauthorized_fields]).to include(:data_policy) + end + + it "does not include the mac_address" do + expect(presenter.as_json[:mac_address]).to be(nil) + end + + it "includes the mac_address in the unauthorized_fields" do + expect(presenter.as_json[:unauthorized_fields]).to include(:mac_address) + end + end + + +end diff --git a/spec/presenters/measurement_presenter_spec.rb b/spec/presenters/measurement_presenter_spec.rb new file mode 100644 index 00000000..6c6ed5c3 --- /dev/null +++ b/spec/presenters/measurement_presenter_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" +describe Presenters::MeasurementPresenter do + + let(:measurement) { + FactoryBot.create(:measurement) + } + + let(:current_user) { + FactoryBot.create(:user) + } + + let(:render_context) { + double(:render_context) + } + + let(:options) { + {} + } + + subject(:presenter) { Presenters::MeasurementPresenter.new(measurement, current_user, render_context, options) } + + it "exposes the id" do + expect(presenter.as_json[:id]).to eq(measurement.id) + end + + it "exposes the name" do + expect(presenter.as_json[:name]).to eq(measurement.name) + end + + it "exposes the description" do + expect(presenter.as_json[:description]).to eq(measurement.description) + end + + it "exposes the unit" do + expect(presenter.as_json[:unit]).to eq(measurement.unit) + end + + it "exposes the uuid" do + expect(presenter.as_json[:uuid]).to eq(measurement.uuid) + end + + it "exposes the description" do + expect(presenter.as_json[:description]).to eq(measurement.description) + end +end diff --git a/spec/presenters/sensor_presenter_spec.rb b/spec/presenters/sensor_presenter_spec.rb new file mode 100644 index 00000000..a4566b7f --- /dev/null +++ b/spec/presenters/sensor_presenter_spec.rb @@ -0,0 +1,74 @@ +require "rails_helper" +describe Presenters::SensorPresenter do + + let(:sensor) { + FactoryBot.create(:sensor) + } + + let(:current_user) { + FactoryBot.create(:user) + } + + let(:measurement_presentation) { + double(:measurement_presentation) + } + + let(:render_context) { + double(:render_context) + } + + let(:options) { + {} + } + + subject(:presenter) { Presenters::SensorPresenter.new(sensor, current_user, render_context, options) } + + it "exposes the id" do + expect(presenter.as_json[:id]).to eq(sensor.id) + end + + it "exposes the parent_id" do + expect(presenter.as_json[:parent_id]).to eq(sensor.parent_id) + end + + it "exposes the name" do + expect(presenter.as_json[:name]).to eq(sensor.name) + end + + it "exposes the description" do + expect(presenter.as_json[:description]).to eq(sensor.description) + end + + it "exposes the unit" do + expect(presenter.as_json[:unit]).to eq(sensor.unit) + end + + it "exposes the created_at date" do + expect(presenter.as_json[:created_at]).to eq(sensor.created_at) + end + + it "exposes the updated_at date" do + expect(presenter.as_json[:updated_at]).to eq(sensor.updated_at) + end + + it "exposes the uuid" do + expect(presenter.as_json[:uuid]).to eq(sensor.uuid) + end + + it "exposes the datasheet" do + expect(presenter.as_json[:datasheet]).to eq(sensor.datasheet) + end + + it "exposes the unit_definition" do + expect(presenter.as_json[:unit_definition]).to eq(sensor.unit_definition) + end + + it "exposes the tags" do + expect(presenter.as_json[:tags]).to eq(sensor.tags) + end + + it "exposes a presentation of the measurement" do + allow(Presenters).to receive(:present).with(sensor.measurement, current_user, render_context, {}).and_return(measurement_presentation) + expect(presenter.as_json[:measurement]).to eq(measurement_presentation) + end +end diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb new file mode 100644 index 00000000..74aad809 --- /dev/null +++ b/spec/presenters/user_presenter_spec.rb @@ -0,0 +1,144 @@ +require "rails_helper" +describe Presenters::UserPresenter do + + let(:user) { + FactoryBot.create(:user) + } + + let(:current_user) { + FactoryBot.create(:user) + } + + let(:device_1) { + FactoryBot.create(:device) + } + + let(:device_2) { + FactoryBot.create(:device) + } + + let(:device_1_presentation) { + double(:device_1_presentation) + } + + let(:device_2_presentation) { + double(:device_2_presentation) + } + + let(:user) { + FactoryBot.create(:user, devices: [device_1, device_2]) + } + + let(:profile_picture_url) { + double(:profile_picture_url) + } + + let(:render_context) { + double(:render_context).tap do |render_context| + allow(render_context).to receive(:profile_picture_url).and_return(profile_picture_url) + end + } + + let(:options) { + {} + } + + let(:show_private_info) do + false + end + + let(:user_policy) { + double(:user_policy).tap do |user_policy| + allow(user_policy).to receive(:show_private_info?).and_return(show_private_info) + end + } + + before do + allow(UserPolicy).to receive(:new).and_return(user_policy) + allow(Presenters).to receive(:present).and_return([device_1_presentation, device_2_presentation]) + end + + subject(:presenter) { Presenters::UserPresenter.new(user, current_user, render_context, options) } + + it "exposes the id" do + expect(presenter.as_json[:id]).to eq(user.id) + end + + it "exposes the uuid" do + expect(presenter.as_json[:uuid]).to eq(user.uuid) + end + + it "exposes the role" do + expect(presenter.as_json[:role]).to eq(user.role) + end + + it "exposes the username" do + expect(presenter.as_json[:username]).to eq(user.username) + end + + it "gets the profile_picture_url from the render_context" do + expect(presenter.as_json[:profile_picture]).to eq(profile_picture_url) + expect(render_context).to have_received(:profile_picture_url).with(user) + end + + it "exposes the location" do + expect(presenter.as_json[:location]).to eq(user.location) + end + + it "exposes the created_at date" do expect(presenter.as_json[:created_at]).to eq(user.created_at) + end + + it "exposes the updated_at date" do + expect(presenter.as_json[:updated_at]).to eq(user.updated_at) + end + + context "when the current user is not authorized" do + it "does not show the email" do + expect(presenter.as_json[:email]).to eq(nil) + end + + it "includes the email field in the unauthorized_fields collection" do + expect(presenter.as_json[:unauthorized_fields]).to include(:email) + end + + it "does not show the legacy API key" do + expect(presenter.as_json[:legacy_api_key]).to eq(nil) + end + + it "includes the legacy API key field in the unauthorized fields collection" do + expect(presenter.as_json[:unauthorized_fields]).to include(:legacy_api_key) + end + end + + context "when the current user is authorized" do + let(:show_private_info) { true } + + it "shows the email" do + expect(presenter.as_json[:email]).to eq(user.email) + end + + it "shows the legacy API key" do + expect(presenter.as_json[:legacy_api_key]).to eq(user.legacy_api_key) + end + + it "does not include any unauthorized fields" do + expect(presenter.as_json).not_to include(:unauthorized_fields) + end + end + + context "by default" do + it "includes presentations of the user's devices" do + + expect(presenter.as_json[:devices]).to eq([device_1_presentation, device_2_presentation]) + expect(Presenters).to have_received(:present).with([device_1, device_2], current_user, render_context, options) + end + end + + context "when the with_devices option is false" do + let(:options) { { with_devices: false }} + + it "does not include devices" do + expect(presenter.as_json[:devices]).to eq(nil) + end + end +end