Files
ubicloud/spec/routes/web/load_balancer_spec.rb
Jeremy Evans f721c31c55 Avoid the use of serializers for the load balancer show template
This allows us to remove the :load_balancer option from the VM
serializer and the :vms_serialized option from the LoadBalancer
serializer.

Add some model methods for code that was previously in the
serializers.

To a better job of testing for expected values on the load
balancer show page, instead of using the non-specific
have_content for everything.
2025-08-08 01:52:14 +09:00

375 lines
16 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
RSpec.describe Clover, "load balancer" do
let(:user) { create_account }
let(:project) { user.create_project_with_default_policy("project-1") }
let(:project_wo_permissions) { user.create_project_with_default_policy("project-2", default_policy: nil) }
let(:lb) do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = LoadBalancer.create(private_subnet_id: ps.id, name: "dummy-lb-1", health_check_endpoint: "/up", project_id: project.id)
LoadBalancerPort.create(load_balancer_id: lb.id, src_port: 80, dst_port: 8080)
lb
end
let(:lb_wo_permission) {
ps = Prog::Vnet::SubnetNexus.assemble(project_wo_permissions.id, name: "dummy-ps-2", location_id: Location::HETZNER_FSN1_ID).subject
lb = LoadBalancer.create(private_subnet_id: ps.id, name: "dummy-lb-2", health_check_endpoint: "/up", project_id: project_wo_permissions.id)
LoadBalancerPort.create(load_balancer_id: lb.id, src_port: 80, dst_port: 8080)
lb
}
describe "unauthenticated" do
it "can not list without login" do
visit "/load-balancer"
expect(page.title).to eq("Ubicloud - Login")
end
it "can not create without login" do
visit "/load-balancer/create"
expect(page.title).to eq("Ubicloud - Login")
end
end
describe "authenticated" do
before do
login(user.email)
end
describe "list" do
it "can list no load balancers" do
visit "#{project.path}/load-balancer"
expect(page.title).to eq("Ubicloud - Load Balancers")
expect(page).to have_content "No Load Balancers"
click_link "Create Load Balancer"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
end
it "can not list load balancers when does not have permissions" do
lb
lb_wo_permission
visit "#{project.path}/load-balancer"
expect(page.title).to eq("Ubicloud - Load Balancers")
expect(page).to have_content lb.name
expect(page).to have_no_content lb_wo_permission.name
expect(page).to have_no_content "Waiting for hostname to be ready"
expect(page).to have_content "Create Load Balancer"
AccessControlEntry.dataset.destroy
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["LoadBalancer:view"])
page.refresh
expect(page).to have_no_content "Create Load Balancer"
end
it "handles case where the user does not have permissions to create load balancers" do
visit "#{project.path}/load-balancer"
expect(page).to have_content "Get started by creating a new Load Balancer."
expect(page).to have_no_content "You don't have permission to create Load Balancers."
AccessControlEntry.dataset.destroy
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["LoadBalancer:view"])
page.refresh
expect(page).to have_content "You don't have permission to create Load Balancers."
expect(page).to have_no_content "Get started by creating a new Load Balancer."
end
end
describe "create" do
it "can create new load balancer" do
project
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
visit "#{project.path}/load-balancer/create"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
name = "dummy-lb-1"
fill_in "Name", with: name
fill_in "Load Balancer Port", with: 80
fill_in "Application Port", with: 8000
select "Round Robin", from: "algorithm"
fill_in "HTTP Health Check Endpoint", with: "/up"
select ps.name, from: "private_subnet_id"
select "HTTP", from: "health_check_protocol"
click_button "Create"
expect(page.title).to eq("Ubicloud - #{name}")
expect(page).to have_flash_notice("'#{name}' is created")
expect(LoadBalancer.count).to eq(1)
expect(LoadBalancer.first.project_id).to eq(project.id)
end
it "can not create load balancer with invalid name" do
project
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
visit "#{project.path}/load-balancer/create"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
fill_in "Name", with: "invalid name"
fill_in "Load Balancer Port", with: 80
fill_in "Application Port", with: 8000
select "Round Robin", from: "algorithm"
fill_in "HTTP Health Check Endpoint", with: "/up"
select ps.name, from: "private_subnet_id"
select "HTTP", from: "health_check_protocol"
click_button "Create"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
expect(page).to have_content "Name must only contain"
expect((find "input[name=name]")["value"]).to eq("invalid name")
end
it "can not create load balancer in a project when does not have permissions" do
Prog::Vnet::SubnetNexus.assemble(project_wo_permissions.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
visit "#{project_wo_permissions.path}/load-balancer/create"
expect(page.title).to eq("Ubicloud - Forbidden")
expect(page.status_code).to eq(403)
expect(page).to have_content "Forbidden"
end
it "can not create load balancer with invalid private subnet" do
project
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
visit "#{project.path}/load-balancer/create"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
fill_in "Name", with: "dummy-lb-1"
fill_in "Load Balancer Port", with: 80
fill_in "Application Port", with: 8000
select "Round Robin", from: "algorithm"
fill_in "HTTP Health Check Endpoint", with: "/up"
select ps.name, from: "private_subnet_id"
select "HTTP", from: "health_check_protocol"
ps.destroy
click_button "Create"
expect(page.title).to eq("Ubicloud - Create Load Balancer")
expect(page).to have_content "Private subnet not found"
end
end
describe "show" do
it "can show load balancer details" do
lb
visit "#{project.path}/load-balancer"
expect(page.title).to eq("Ubicloud - Load Balancers")
expect(page).to have_content lb.name
expect(page).to have_content lb.hostname
click_link lb.name, href: "#{project.path}#{lb.path}"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_content lb.name
expect(page).to have_content "Round Robin"
end
it "raises forbidden when does not have permissions" do
visit "#{project_wo_permissions.path}/location/eu-central-h1/load-balancer/#{lb_wo_permission.name}"
expect(page.title).to eq("Ubicloud - Forbidden")
expect(page.status_code).to eq(403)
expect(page).to have_content "Forbidden"
end
it "raises not found when load balancer not exists" do
visit "#{project.path}/location/eu-central-h1/load-balancer/08s56d4kaj94xsmrnf5v5m3mav"
expect(page.title).to eq("Ubicloud - ResourceNotFound")
expect(page.status_code).to eq(404)
expect(page).to have_content "ResourceNotFound"
end
end
describe "load-balancers" do
it "can show" do
visit "#{project.path}#{lb.path}"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_content lb.name
expect(page).to have_content lb.private_subnet.name
expect(page).to have_content "Round Robin"
expect(page).to have_content "/up"
end
it "can attach vm" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000, algorithm: "hash_based").subject
dz = DnsZone.create(name: "test-dns-zone", project_id: project.id)
cert = Prog::Vnet::CertNexus.assemble("test-host-name", dz.id).subject
cert.update(cert: "cert", csr_key: Clec::Cert.ec_key.to_der)
lb.add_cert(cert)
vm = Prog::Vm::Nexus.assemble("k y", project.id, name: "dummy-vm-1", private_subnet_id: ps.id).subject
visit "#{project.path}#{lb.path}"
select vm.name, from: "vm_id"
click_button "Attach"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_flash_notice("VM is attached to the load balancer")
expect(lb.vms.count).to eq(1)
expect(Config).to receive(:load_balancer_service_hostname).and_return("lb.ubicloud.com")
visit "#{project.path}#{lb.path}"
expect(page.all("dt,dd").map(&:text)).to eq [
"ID", lb.ubid,
"Name", "dummy-lb-3",
"Connection String", "dummy-lb-3.#{ps.ubid[-5...]}.lb.ubicloud.com",
"Private Subnet", "dummy-ps-1",
"Algorithm", "Hash Based",
"Stack", "dual",
"Load Balancer Port", "80",
"Application Port", "8000",
"HTTP Health Check Endpoint", "/up"
]
expect(page.all("#lb-vms td").map(&:text)).to eq [
"dummy-vm-1", "down", "Detach",
"Select a VM", "", "Attach"
]
click_link "dummy-ps-1"
expect(page.title).to eq("Ubicloud - #{ps.name}")
end
it "can not attach vm when it is already attached to another load balancer" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb1 = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
lb2 = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-4", src_port: 80, dst_port: 8000).subject
dz = DnsZone.create(name: "test-dns-zone", project_id: project.id)
cert = Prog::Vnet::CertNexus.assemble("test-host-name", dz.id).subject
cert.update(cert: "cert", csr_key: Clec::Cert.ec_key.to_der)
lb1.add_cert(cert)
vm = Prog::Vm::Nexus.assemble("k y", project.id, name: "dummy-vm-1", private_subnet_id: ps.id).subject
visit "#{project.path}#{lb2.path}"
select vm.name, from: "vm_id"
lb1.add_vm(vm)
click_button "Attach"
expect(page.title).to eq("Ubicloud - #{lb2.name}")
expect(page).to have_content "VM is already attached to a load balancer"
expect(lb2.vms.count).to eq(0)
end
it "can not attach vm when it does not exist" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
vm = Prog::Vm::Nexus.assemble("k y", project.id, name: "dummy-vm-1", private_subnet_id: ps.id).subject
visit "#{project.path}#{lb.path}"
select vm.name, from: "vm_id"
vm.nics.first.destroy
vm.destroy
click_button "Attach"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_content "VM not found"
expect(lb.vms.count).to eq(0)
end
it "can detach vm" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
dz = DnsZone.create(name: "test-dns-zone", project_id: project.id)
cert = Prog::Vnet::CertNexus.assemble("test-host-name", dz.id).subject
cert.update(cert: "cert", csr_key: Clec::Cert.ec_key.to_der)
lb.add_cert(cert)
vm = Prog::Vm::Nexus.assemble("k y", project.id, name: "dummy-vm-1", private_subnet_id: ps.id).subject
expect(page).to have_no_content vm.name
lb.add_vm(vm)
visit "#{project.path}#{lb.path}"
expect(page).to have_content vm.name
click_button "Detach"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_flash_notice("VM is detached from the load balancer")
expect(Strand.where(prog: "Vnet::LoadBalancerHealthProbes").all.count { |st| st.stack[0]["subject_id"] == lb.id && st.stack[0]["vm_id"] == vm.id }).to eq(0)
expect(lb.update_load_balancer_set?).to be(true)
expect(lb.vm_ports_dataset.where(load_balancer_vm_id: LoadBalancerVm.where(vm_id: vm.id).select(:id)).first&.state).to eq("detaching")
end
it "can not detach vm when it does not exist" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
dz = DnsZone.create(name: "test-dns-zone", project_id: project.id)
cert = Prog::Vnet::CertNexus.assemble("test-host-name", dz.id).subject
cert.update(cert: "cert", csr_key: Clec::Cert.ec_key.to_der)
lb.add_cert(cert)
vm = Prog::Vm::Nexus.assemble("k y", project.id, name: "dummy-vm-1", private_subnet_id: ps.id).subject
visit "#{project.path}#{lb.path}"
select "dummy-vm-1", from: "vm_id"
click_button "Attach"
visit "#{project.path}#{lb.path}"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(lb.reload.vms.count).to eq(1)
vm.nics.first.destroy
vm.destroy
click_button "Detach"
expect(page.title).to eq("Ubicloud - #{lb.name}")
expect(page).to have_content "VM not found"
expect(lb.reload.vms.count).to eq(0)
end
end
describe "delete" do
it "can delete load balancer" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
visit "#{project.path}#{lb.path}"
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
# UI tests run without a JavaScript enginer.
btn = find ".delete-btn"
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
expect(lb.destroy_set?).to be true
end
it "can not delete load balancer when does not have permissions" do
# Give permission to view, so we can see the detail page
AccessControlEntry.create(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["LoadBalancer:view"])
visit "#{project_wo_permissions.path}#{lb_wo_permission.path}"
expect(page.title).to eq "Ubicloud - dummy-lb-2"
expect { find ".delete-btn" }.to raise_error Capybara::ElementNotFound
end
it "can not delete load balancer when it doesn't exist" do
ps = Prog::Vnet::SubnetNexus.assemble(project.id, name: "dummy-ps-1", location_id: Location::HETZNER_FSN1_ID).subject
lb = Prog::Vnet::LoadBalancerNexus.assemble(ps.id, name: "dummy-lb-3", src_port: 80, dst_port: 8000).subject
visit "#{project.path}#{lb.path}"
lb.update(name: "new-name")
# We send delete request manually instead of just clicking to button because delete action triggered by JavaScript.
# UI tests run without a JavaScript enginer.
btn = find ".delete-btn"
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
expect(page.status_code).to eq(204)
end
end
end
end