diff --git a/app/models/device/volume_details.rb b/app/models/device/volume_details.rb new file mode 100644 index 000000000..dfd6010f6 --- /dev/null +++ b/app/models/device/volume_details.rb @@ -0,0 +1,15 @@ +class Device::VolumeDetails < Device::Details + + validate :device_uses_volume_template + + private + + def device_uses_volume_template + reload_device + return unless device.present? + unless device.template.tag == 'volume' + self.errors.add(:device, 'must use the `volume` template if it has a Device::VolumeDetails') + end + end + +end diff --git a/app/presenters/device/compute_details_presenter.rb b/app/presenters/device/compute_details_presenter.rb index 489e99a64..8922e3bf6 100644 --- a/app/presenters/device/compute_details_presenter.rb +++ b/app/presenters/device/compute_details_presenter.rb @@ -2,8 +2,38 @@ class Device::ComputeDetailsPresenter < Device::DetailsPresenter delegate :public_ips, :private_ips, :login_user, :ssh_key, :volume_details, to: :o - def is_compute_device? - true + def additional_details + [].tap do |d| + if has_login_details? + d << [ + 'Access details:', + { + 'Login user:': login_user || 'Unknown', + 'Public IPs:': public_ips, + 'Private IPs:': private_ips, + 'SSH key:': ssh_key || 'Unknown' + } + ] + end + + if has_volume_details? + d << [ + 'Volume details:', volume_details + ] + end + + end + end + + + private + + def has_login_details? + public_ips || private_ips || ssh_key || login_user + end + + def has_volume_details? + !volume_details.empty? end end diff --git a/app/presenters/device/details_presenter.rb b/app/presenters/device/details_presenter.rb index 13f66a763..18b60c5cc 100644 --- a/app/presenters/device/details_presenter.rb +++ b/app/presenters/device/details_presenter.rb @@ -1,7 +1,7 @@ class Device::DetailsPresenter < Presenter - def is_compute_device? - false + def additional_details + [] end end diff --git a/app/presenters/device/network_details_presenter.rb b/app/presenters/device/network_details_presenter.rb index eca7571c1..aa66af6cb 100644 --- a/app/presenters/device/network_details_presenter.rb +++ b/app/presenters/device/network_details_presenter.rb @@ -1,2 +1,15 @@ class Device::NetworkDetailsPresenter < Device::DetailsPresenter + + def additional_details + { + 'Admin state up:': o.admin_state_up, + 'DNS domain': o.dns_domain, + 'L2 adjacency:': o.l2_adjacency, + 'MTU:': o.mtu, + 'Port security enabled:': o.port_security_enabled, + 'Shared:': o.shared, + 'QoS policy:': o.qos_policy + } + end + end diff --git a/app/presenters/device/volume_details_presenter.rb b/app/presenters/device/volume_details_presenter.rb new file mode 100644 index 000000000..bf98359e2 --- /dev/null +++ b/app/presenters/device/volume_details_presenter.rb @@ -0,0 +1,14 @@ +class Device::VolumeDetailsPresenter < Device::DetailsPresenter + + def additional_details + { + 'Availability zone:': o.availability_zone, + 'Bootable:': o.bootable, + 'Encrypted:': o.encrypted, + 'Read-only:': o.read_only, + 'Size (GB):': o.size, + 'Volume type:': o.volume_type + } + end + +end diff --git a/app/presenters/device_presenter.rb b/app/presenters/device_presenter.rb index 82ccfdbb6..4fccf06eb 100644 --- a/app/presenters/device_presenter.rb +++ b/app/presenters/device_presenter.rb @@ -17,7 +17,7 @@ class DevicePresenter < Presenter delegate :vcpus, :ram, :disk, to: :template - delegate :is_compute_device?, to: :details + delegate :additional_details, to: :details # location returns the location of the device. For devices in simple # chassis, the chassis's location is returned. Devices in complex chassis, @@ -56,22 +56,6 @@ def details h.presenter_for(o.details) end - def has_login_details? - is_compute_device? && (details.public_ips || details.private_ips || details.ssh_key || details.login_user) - end - - def login_user - o.details.login_user.presence || h.content_tag(:em, 'Unknown') - end - - def ssh_key - o.details.ssh_key.presence || h.content_tag(:em, 'Unknown') - end - - def has_volume_details? - is_compute_device? && !o.details.volume_details.empty? - end - def has_metadata? !metadata.empty? end diff --git a/app/services/template_services/create.rb b/app/services/template_services/create.rb index 9584aa1f2..9831493ea 100644 --- a/app/services/template_services/create.rb +++ b/app/services/template_services/create.rb @@ -39,12 +39,12 @@ def build_template(params) rack_repeat_ratio: nil, tag: params[:tag], - images: params[:images] + # Users can't upload images here, only use existing ones (though I + # suppose they could use fully-qualified URLs too) + images: params[:images] || {} ) if template.images.empty? - # Users can't upload images here, only use existing ones (though I - # suppose they could use fully-qualified URLs too) set_images_from_height(template) end diff --git a/app/views/api/v1/devices/details/volume_details.rabl b/app/views/api/v1/devices/details/volume_details.rabl new file mode 100644 index 000000000..dbe9f940e --- /dev/null +++ b/app/views/api/v1/devices/details/volume_details.rabl @@ -0,0 +1,4 @@ +object @details + +attributes :availability_zone, :bootable, :encrypted, :read_only, :size, + :volume_type diff --git a/app/views/devices/show.html.erb b/app/views/devices/show.html.erb index df376c5d0..3270673ed 100644 --- a/app/views/devices/show.html.erb +++ b/app/views/devices/show.html.erb @@ -12,21 +12,8 @@ dl.item "Status:", device.status dl.item "Cost this billing period:", device.currency_cost - if device.has_login_details? - dl.sublist "Access details:" do |sl| - sl.item "Login user:", device.login_user - sl.item "Public IPs:", device.public_ips - sl.item "Private IPs:", device.private_ips - sl.item "SSH Key:", device.ssh_key - end - end - - if device.has_volume_details? - dl.sublist "Volume details:" do |sl| - device.volume_details.each do |key, val| - sl.item key, val - end - end + device.additional_details.each do |label, details_list| + dl.recurse_items label, details_list end dl.sublist "Template:" do |sl| diff --git a/db/migrate/20240216141238_seed_network_device_template.rb b/db/migrate/20240216141238_seed_network_device_template.rb index 10fa7f3e9..25c4c2f17 100644 --- a/db/migrate/20240216141238_seed_network_device_template.rb +++ b/db/migrate/20240216141238_seed_network_device_template.rb @@ -1,5 +1,11 @@ class SeedNetworkDeviceTemplate < ActiveRecord::Migration[7.1] + + class Template < ApplicationRecord + enum rackable: { rackable: 1, zerouable: 2, nonrackable: 3 } + end + def change + Template.reset_column_information reversible do |dir| dir.up do diff --git a/db/migrate/20240219161225_migrate_default_rack_template_to_tag.rb b/db/migrate/20240219161225_migrate_default_rack_template_to_tag.rb index 27aa733ac..d3769f0bf 100644 --- a/db/migrate/20240219161225_migrate_default_rack_template_to_tag.rb +++ b/db/migrate/20240219161225_migrate_default_rack_template_to_tag.rb @@ -3,6 +3,7 @@ class MigrateDefaultRackTemplateToTag < ActiveRecord::Migration[7.1] class Template < ApplicationRecord; end def change + Template.reset_column_information reversible do |dir| dir.up do # Intentionally not using Template.default_rack_template here! diff --git a/db/migrate/20240220115416_create_device_volume_details.rb b/db/migrate/20240220115416_create_device_volume_details.rb new file mode 100644 index 000000000..841dc71a7 --- /dev/null +++ b/db/migrate/20240220115416_create_device_volume_details.rb @@ -0,0 +1,14 @@ +class CreateDeviceVolumeDetails < ActiveRecord::Migration[7.1] + def change + create_table :device_volume_details, id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string :availability_zone + t.boolean :bootable + t.boolean :encrypted + t.boolean :read_only + t.integer :size + t.string :volume_type + + t.timestamps + end + end +end diff --git a/db/migrate/20240220150125_seed_volume_device_template.rb b/db/migrate/20240220150125_seed_volume_device_template.rb new file mode 100644 index 000000000..29ea989e4 --- /dev/null +++ b/db/migrate/20240220150125_seed_volume_device_template.rb @@ -0,0 +1,38 @@ +class SeedVolumeDeviceTemplate < ActiveRecord::Migration[7.1] + + class Template < ApplicationRecord + enum rackable: { rackable: 1, zerouable: 2, nonrackable: 3 } + end + + def change + Template.reset_column_information + reversible do |dir| + + dir.up do + t = Template.new( + name: 'volume', + template_type: 'Device', + tag: 'volume', + version: 1, + height: 2, + depth: 2, + rows: 1, + columns: 1, + rackable: 'rackable', + simple: true, + description: 'Volume', + images: { + 'front' => 'disk_front_2u.png', + 'rear' => 'generic_rear_2u.png', + } + ) + t.save! + end + + dir.down do + Template.find_by_tag('volume')&.destroy! + end + + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a56bcda9..b0ec9c942 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_02_19_161225) do +ActiveRecord::Schema[7.1].define(version: 2024_02_20_150125) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -90,6 +90,17 @@ t.datetime "updated_at", null: false end + create_table "device_volume_details", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "availability_zone" + t.boolean "bootable" + t.boolean "encrypted" + t.boolean "read_only" + t.integer "size" + t.string "volume_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "devices", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", limit: 255, null: false t.string "description", limit: 255 diff --git a/docs/api/examples/README.md b/docs/api/examples/README.md index 12bf20aa7..28c5822eb 100644 --- a/docs/api/examples/README.md +++ b/docs/api/examples/README.md @@ -171,6 +171,15 @@ facing are `f` or `b` for front and back respectively. ./create-device.sh ``` +To create network and volume devices use the following scripts. They are +similar to the `create-device.sh` script except the template id is determined +automatically. + +``` +./create-network.sh +./create-volume.sh +``` + Move a device. ``` diff --git a/docs/api/examples/create-and-populate-rack.sh b/docs/api/examples/create-and-populate-rack.sh index 8c9605cb5..4b31c6386 100755 --- a/docs/api/examples/create-and-populate-rack.sh +++ b/docs/api/examples/create-and-populate-rack.sh @@ -26,8 +26,16 @@ echo "Created empty rack ${RACK_NAME}" >&2 "${SCRIPT_DIR}/show-rack.sh" "${RACK_ID}" echo + +OUTPUT=$("${SCRIPT_DIR}/create-volume.sh" vol-1 "${RACK_ID}" f 1) +if [ $? -ne 0 ] ; then + # Errors will have been sent to stderr. + exit +fi +echo "Created volume device" + # Create a badly named and located device in that empty rack. -LARGEST_TEMPLATE=$( "${SCRIPT_DIR}/list-templates.sh" | jq "sort_by(.height) | reverse | .[0]" ) +LARGEST_TEMPLATE=$( "${SCRIPT_DIR}/list-templates.sh" | jq "map(select(.tag == null)) | sort_by(.height) | reverse | .[0]" ) TEMPLATE_ID=$(echo "${LARGEST_TEMPLATE}" | jq -r .id) TEMPLATE_HEIGHT=$(echo "${LARGEST_TEMPLATE}" | jq -r .height) START_U=$(( ${RACK_HEIGHT} - ${TEMPLATE_HEIGHT} + 1 )) @@ -52,7 +60,7 @@ echo "Renamed device" >&2 echo # Correct the location of the device. -OUTPUT=$("${SCRIPT_DIR}/move-device.sh" "${DEVICE_ID}" "${RACK_ID}" f 1) +OUTPUT=$("${SCRIPT_DIR}/move-device.sh" "${DEVICE_ID}" "${RACK_ID}" f 3) if [ $? -ne 0 ] ; then # Errors will have been sent to stderr. exit @@ -69,8 +77,8 @@ fi echo "Created network device" # Leave some space at the top to allow dragging devices around the IRV. -START_U=$(( ${TEMPLATE_HEIGHT} + 1 )) -END_U=$(( $RACK_HEIGHT - 4 )) +START_U=$(( ${TEMPLATE_HEIGHT} + 3 )) +END_U=$(( $RACK_HEIGHT - 6 )) "${SCRIPT_DIR}/populate-rack.sh" ${RACK_ID} ${START_U} ${END_U} comp102 "${SCRIPT_DIR}/show-rack.sh" "${RACK_ID}" diff --git a/docs/api/examples/create-network.sh b/docs/api/examples/create-network.sh index 3e3f6858e..dffc6b0c5 100755 --- a/docs/api/examples/create-network.sh +++ b/docs/api/examples/create-network.sh @@ -19,7 +19,7 @@ RACK_ID=${2} FACING=${3} START_U=${4} -NETWORK_TEMPLATE_ID=$( "${SCRIPT_DIR}/list-templates.sh" | jq -r "sort_by(.height) | (.[] | select(.tag | . and contains(\"network\"))) | .id" ) +NETWORK_TEMPLATE_ID=$( "${SCRIPT_DIR}/list-templates.sh" | jq -r '.[] | select(.tag == "network") | .id' ) if [ -z "${NETWORK_TEMPLATE_ID}" ]; then echo "Couldn't find a template with tag='network'" diff --git a/docs/api/examples/create-volume.sh b/docs/api/examples/create-volume.sh new file mode 100755 index 000000000..f568f8e42 --- /dev/null +++ b/docs/api/examples/create-volume.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +set -e +set -o pipefail +# set -x + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# The base URL against which relative URLs are constructed. +CONCERTIM_HOST=${CONCERTIM_HOST:-command.concertim.alces-flight.com} +BASE_URL="https://${CONCERTIM_HOST}/api/v1" + +# Use the specified AUTH_TOKEN or generate one. If AUTH_TOKEN is being +# generated LOGIN and PASSWORD environment variables must be set. +AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} + +NAME=${1} +RACK_ID=${2} +FACING=${3} +START_U=${4} + +VOLUME_TEMPLATE_ID=$( "${SCRIPT_DIR}/list-templates.sh" | jq -r '.[] | select(.tag == "volume") | .id' ) + +if [ -z "${VOLUME_TEMPLATE_ID}" ]; then + echo "Couldn't find a template with tag='volume'" + exit 1 +fi + +# The metadata below is hardcoded but it could be any valid JSON document. + +BODY=$(jq --null-input \ + --arg name "${NAME}" \ + --arg description "This is ${NAME} volume" \ + --arg facing "${FACING}" \ + --arg start_u "${START_U}" \ + --arg rack_id "${RACK_ID}" \ + --arg template_id "${VOLUME_TEMPLATE_ID}" \ + ' +{ + "template_id": $template_id, + "device": { + "name": $name, + "description": $description, + "location": { + "facing": $facing, + "rack_id": $rack_id, + "start_u": $start_u|tonumber + }, + "status": "IN_PROGRESS", + "metadata": { + "openstack_instance_id": "8f4e9068-5a39-4717-8a83-6b95e01031eb", + "status": ["build", "scheduling", ""] + }, + "details": { + "type": "Device::VolumeDetails", + "bootable": false, + "encrypted": false, + "size": 2 + } + } +} +' +) + +# Run curl with funky redirection to capture response body and status code. +BODY_FILE=$(mktemp) +HTTP_STATUS=$( +curl -s -k \ + -w "%{http_code}" \ + -o >(cat > "${BODY_FILE}") \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -X POST "${BASE_URL}/nodes" \ + -d "${BODY}" +) + +if [ "${HTTP_STATUS}" == "200" ] || [ "${HTTP_STATUS}" == "201" ] ; then + cat "$BODY_FILE" +else + echo "Device creation failed" >&2 + cat "$BODY_FILE" >&2 + exit 1 +fi diff --git a/docs/api/examples/populate-network-template.sh b/docs/api/examples/populate-network-template.sh index ecbc667af..d0d15346e 100755 --- a/docs/api/examples/populate-network-template.sh +++ b/docs/api/examples/populate-network-template.sh @@ -24,7 +24,8 @@ BODY=$(jq --null-input ' "images": { "front": "switch_front_1u.png", "rear": "switch_rear_1u.png" - } + }, + "rackable": "rackable" } } ' \ diff --git a/docs/api/examples/populate-rack.sh b/docs/api/examples/populate-rack.sh index 2156ad8b6..f8cd01863 100755 --- a/docs/api/examples/populate-rack.sh +++ b/docs/api/examples/populate-rack.sh @@ -28,7 +28,7 @@ fi RACK_HEIGHT=$(echo "${OUTPUT}" | jq -r .u_height) RACK_NAME=$(echo "${OUTPUT}" | jq -r .name) -SMALL_TEMPLATE_ID=$( "${SCRIPT_DIR}/list-templates.sh" | jq -r "sort_by(.height) | .[0] | .id" ) +SMALL_TEMPLATE_ID=$( "${SCRIPT_DIR}/list-templates.sh" | jq -r "map(select(.tag == null)) | sort_by(.height) | .[0] | .id" ) for i in $(seq -w 0 $(( 10#${END_U} - 10#${FIRST_U} )) ) ; do # i=$(( 10#${i} - 1 )) sleep 0.5 diff --git a/docs/api/examples/populate-volume-template.sh b/docs/api/examples/populate-volume-template.sh new file mode 100755 index 000000000..504f1480d --- /dev/null +++ b/docs/api/examples/populate-volume-template.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e +set -o pipefail +# set -x + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# The base URL against which relative URLs are constructed. +CONCERTIM_HOST=${CONCERTIM_HOST:-command.concertim.alces-flight.com} +BASE_URL="https://${CONCERTIM_HOST}/api/v1" + +# Use the specified AUTH_TOKEN or generate one. If AUTH_TOKEN is being +# generated LOGIN and PASSWORD environment variables must be set. +AUTH_TOKEN=${AUTH_TOKEN:-$("${SCRIPT_DIR}"/get-auth-token.sh)} + +BODY=$(jq --null-input ' +{ + "template": { + "name": "volume", + "description": "Volume", + "height": 2, + "tag": "volume", + "images": { + "front": "disk_front_2u.png", + "rear": "generic_rear_2u.png" + }, + "rackable": "rackable" + } +} +' \ + | jq '{template: .template | with_entries(select(.value != ""))}' +) + +# Run curl with funky redirection to capture response body and status code. +BODY_FILE=$(mktemp) +HTTP_STATUS=$( +curl -s -k \ + -w "%{http_code}" \ + -o >(cat > "${BODY_FILE}") \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -X POST "${BASE_URL}/templates" \ + -d "${BODY}" +) + +if [ "${HTTP_STATUS}" == "200" ] || [ "${HTTP_STATUS}" == "201" ] ; then + cat "$BODY_FILE" +else + echo "Template creation failed" >&2 + cat "$BODY_FILE" >&2 + exit 1 +fi diff --git a/public/images/irv/concertim/disk_front_2u.png b/public/images/irv/concertim/disk_front_2u.png new file mode 100644 index 000000000..81a808848 Binary files /dev/null and b/public/images/irv/concertim/disk_front_2u.png differ diff --git a/spec/factories/device/volume_details.rb b/spec/factories/device/volume_details.rb new file mode 100644 index 000000000..33dcfe1cc --- /dev/null +++ b/spec/factories/device/volume_details.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :device_volume_details, class: 'Device::VolumeDetails' do + bootable { false } + size { 1 } + read_only { false } + end +end diff --git a/spec/factories/templates.rb b/spec/factories/templates.rb index e3c75e487..545a5cf35 100644 --- a/spec/factories/templates.rb +++ b/spec/factories/templates.rb @@ -33,4 +33,13 @@ height { 1 } tag { "network" } end + + trait :volume_device_template do + template_type { "Device" } + rackable { "rackable" } + rows { 1 } + columns { 1 } + height { 2 } + tag { "volume" } + end end diff --git a/spec/models/device/volume_details_spec.rb b/spec/models/device/volume_details_spec.rb new file mode 100644 index 000000000..99d0cf702 --- /dev/null +++ b/spec/models/device/volume_details_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe Device::VolumeDetails, type: :model do + let(:user) { create(:user) } + let!(:rack_template) { create(:template, :rack_template) } + let!(:rack) { create(:rack, user: user, template: rack_template) } + let(:location) { create(:location, rack: rack) } + let(:chassis) { create(:chassis, location: location, template: template) } + + let(:device_template) { create(:template, :device_template) } + let(:volume_template) { create(:template, :volume_device_template) } + + let(:device) { create(:device, chassis: chassis, details: described_class.new) } + + subject { device.details } + + describe 'validations' do + context 'for a device using the volume template' do + let(:template) { volume_template } + + it { is_expected.to be_valid } + end + + context 'for a device using the compute device template' do + let(:template) { device_template } + + it { is_expected.not_to be_valid } + it { is_expected.to have_error :device, 'must use the `volume` template if it has a Device::VolumeDetails' } + + it 'also shows error on device' do + expect(device).not_to be_valid + expect(device).to have_error :details, 'is invalid' + end + end + + end +end diff --git a/spec/requests/api/v1/devices_controller_spec.rb b/spec/requests/api/v1/devices_controller_spec.rb index f5f728cd4..4e570a16f 100644 --- a/spec/requests/api/v1/devices_controller_spec.rb +++ b/spec/requests/api/v1/devices_controller_spec.rb @@ -393,6 +393,53 @@ def send_request end end end + + context 'with a volume device' do + let(:device_template) { create(:template, :volume_device_template) } + let(:details) { create(:device_volume_details) } + let!(:device) { create(:device, chassis: chassis, details: details) } + + context "with valid parameters" do + let(:attributes) { + { + device: { + name: device.name + "-updated", + status: "ACTIVE", + details: { + bootable: true + } + } + } + } + def send_request + patch url_under_test, + params: attributes, + headers: headers, + as: :json + end + + it "renders a successful response" do + send_request + expect(response).to have_http_status :ok + end + + it "updates the device" do + expect { + send_request + }.to change{ device.reload.updated_at } + end + + it "includes the device in the response" do + send_request + + parsed_device = JSON.parse(response.body) + expect(parsed_device["name"]).to eq attributes[:device][:name] + expect(parsed_device["status"]).to eq attributes[:device][:status] + parsed_details = parsed_device # ['details'] after revertion of 6b8d3e9 + expect(parsed_details["bootable"]).to eq attributes[:device][:details][:bootable] + end + end + end end context "when not logged in" do diff --git a/spec/requests/api/v1/nodes_controller_spec.rb b/spec/requests/api/v1/nodes_controller_spec.rb index b109011a9..bab54cd63 100644 --- a/spec/requests/api/v1/nodes_controller_spec.rb +++ b/spec/requests/api/v1/nodes_controller_spec.rb @@ -261,6 +261,66 @@ def send_request expect(parsed_details["dns_domain"]).to eq valid_attributes[:device][:details][:dns_domain] end end + + context "with parameters for a volume device" do + let(:device_template) { create(:template, :volume_device_template) } + let(:valid_attributes) { + { + template_id: device_template.id, + device: { + name: "vol-1", + description: "vol-1 description", + location: { + rack_id: rack.id, + start_u: 1, + facing: 'f', + }, + cost: 77.77, + details: { + type: 'Device::VolumeDetails', + bootable: true, + size: 4 + }, + metadata: { "foo" => "bar", "baz" => "qux" }, + status: 'IN_PROGRESS', + } + } + } + + def send_request + post url_under_test, + params: valid_attributes, + headers: headers, + as: :json + end + + it "creates a new device" do + expect { + send_request + }.to change(Device, :count).by(1) + end + + it "renders a successful response" do + send_request + expect(response).to have_http_status :ok + end + + it "includes the device in the response" do + send_request + parsed_device = JSON.parse(response.body) + + expect(parsed_device["name"]).to eq valid_attributes[:device][:name] + expect(parsed_device["description"]).to eq valid_attributes[:device][:description] + expect(parsed_device["metadata"]).to eq valid_attributes[:device][:metadata] + expect(parsed_device["status"]).to eq valid_attributes[:device][:status] + expect(parsed_device["template"]["id"]).to eq valid_attributes[:template_id] + expect(parsed_device["cost"]).to eq "#{'%.2f' % valid_attributes[:device][:cost]}" + + parsed_details = parsed_device #["details"] + expect(parsed_details["bootable"]).to eq valid_attributes[:device][:details][:bootable] + expect(parsed_details["size"]).to eq valid_attributes[:device][:details][:size] + end + end end end end