* Run only two permission queries, one for edit and one for delete, storing the results in local variables. Previously, there were multiple permission queries for view, edit, and/or delete, including an N+1 for edit inside the metric destination loop. The view permissions queries are unnecessary as the route checks for view permissions, so those were eliminated. Unnecessary nested checks for edit permissions were also eliminated, since we don't need to check for changes to permissions during the action. * Fix connection string handling so that "Waiting for host to be ready..." is shown as expected if the resource does not have a connection string. Simply HA display by using a hash constant for expected values. After changes: Line Coverage: 99.95% (11689 / 11695) Branch Coverage: 99.2% (3095 / 3120)
451 lines
18 KiB
Ruby
451 lines
18 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../spec_helper"
|
|
|
|
RSpec.describe Clover, "postgres" 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(:pg) do
|
|
Prog::Postgres::PostgresResourceNexus.assemble(
|
|
project_id: project.id,
|
|
location: "hetzner-fsn1",
|
|
name: "pg-with-permission",
|
|
target_vm_size: "standard-2",
|
|
target_storage_size_gib: 128
|
|
).subject
|
|
end
|
|
|
|
let(:pg_wo_permission) do
|
|
Prog::Postgres::PostgresResourceNexus.assemble(
|
|
project_id: project_wo_permissions.id,
|
|
location: "hetzner-fsn1",
|
|
name: "pg-without-permission",
|
|
target_vm_size: "standard-2",
|
|
target_storage_size_gib: 128
|
|
).subject
|
|
end
|
|
|
|
describe "unauthenticated" do
|
|
it "cannot list without login" do
|
|
visit "/postgres"
|
|
|
|
expect(page.title).to eq("Ubicloud - Login")
|
|
end
|
|
|
|
it "cannot create without login" do
|
|
visit "/postgres/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Login")
|
|
end
|
|
end
|
|
|
|
describe "authenticated" do
|
|
before do
|
|
postgres_project = Project.create_with_id(name: "default").tap { _1.associate_with_project(_1) }
|
|
allow(Config).to receive(:postgres_service_project_id).and_return(postgres_project.id)
|
|
login(user.email)
|
|
|
|
client = instance_double(Minio::Client, list_objects: [])
|
|
allow(Minio::Client).to receive(:new).and_return(client)
|
|
end
|
|
|
|
describe "list" do
|
|
it "can list flavors when there is no pg databases" do
|
|
visit "#{project.path}/postgres"
|
|
|
|
expect(page.title).to eq("Ubicloud - PostgreSQL Databases")
|
|
expect(page).to have_content "Create PostgreSQL Database"
|
|
expect(page).to have_content "Create ParadeDB PostgreSQL Database"
|
|
|
|
click_link "Create PostgreSQL Database"
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
end
|
|
|
|
it "can list only the postgres databases which has permissions to" do
|
|
pg
|
|
pg_wo_permission
|
|
visit "#{project.path}/postgres"
|
|
|
|
expect(page.title).to eq("Ubicloud - PostgreSQL Databases")
|
|
expect(page).to have_content pg.name
|
|
expect(page).to have_no_content pg_wo_permission.name
|
|
end
|
|
end
|
|
|
|
describe "create" do
|
|
it "can create new PostgreSQL database" do
|
|
visit "#{project.path}/postgres/create?flavor=#{PostgresResource::Flavor::STANDARD}"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
name = "new-pg-db"
|
|
fill_in "Name", with: name
|
|
choose option: "eu-central-h1"
|
|
choose option: "standard-2"
|
|
choose option: PostgresResource::HaType::NONE
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{name}")
|
|
expect(page).to have_flash_notice("'#{name}' will be ready in a few minutes")
|
|
expect(PostgresResource.count).to eq(1)
|
|
expect(PostgresResource.first.projects.first.id).to eq(project.id)
|
|
end
|
|
|
|
it "handles errors when creating new PostgreSQL database" do
|
|
visit "#{project.path}/postgres/create?flavor=#{PostgresResource::Flavor::STANDARD}"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
name = "new-pg-db"
|
|
fill_in "Name", with: name
|
|
choose option: "eu-central-h1"
|
|
choose option: "standard-60"
|
|
choose option: PostgresResource::HaType::NONE
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
expect(page).to have_flash_error("Validation failed for following fields: storage_size")
|
|
expect(page).to have_content("Storage size must be one of the following: 1024.0, 2048.0, 4096.0")
|
|
expect(PostgresResource.count).to eq(0)
|
|
end
|
|
|
|
it "can create new ParadeDB PostgreSQL database" do
|
|
expect(Config).to receive(:postgres_paradedb_notification_email).and_return("dummy@mail.com")
|
|
expect(Util).to receive(:send_email)
|
|
visit "#{project.path}/postgres/create?flavor=#{PostgresResource::Flavor::PARADEDB}"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create ParadeDB PostgreSQL Database")
|
|
name = "new-pg-db"
|
|
fill_in "Name", with: name
|
|
choose option: "eu-central-h1"
|
|
choose option: "standard-2"
|
|
choose option: PostgresResource::HaType::NONE
|
|
check "Accept Terms of Service and Privacy Policy"
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{name}")
|
|
expect(page).to have_flash_notice("'#{name}' will be ready in a few minutes")
|
|
expect(PostgresResource.count).to eq(1)
|
|
expect(PostgresResource.first.projects.first.id).to eq(project.id)
|
|
end
|
|
|
|
it "can not open create page with invalid flavor" do
|
|
default_project = Project[name: "Default"]
|
|
url = "#{default_project.path}/dashboard"
|
|
Capybara.current_session.driver.header "Referer", url
|
|
visit "#{project.path}/postgres/create?flavor=invalid"
|
|
|
|
expect(page.title).to eq("Ubicloud - Default Dashboard")
|
|
end
|
|
|
|
it "can not create PostgreSQL database with same name" do
|
|
visit "#{project.path}/postgres/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
|
|
fill_in "Name", with: pg.name
|
|
choose option: "eu-central-h1"
|
|
choose option: "standard-2"
|
|
choose option: PostgresResource::HaType::NONE
|
|
|
|
click_button "Create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
expect(page).to have_flash_error("name is already taken")
|
|
end
|
|
|
|
it "can not select invisible location" do
|
|
visit "#{project.path}/postgres/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create PostgreSQL Database")
|
|
|
|
expect { choose option: "github-runners" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "can not create PostgreSQL database in a project when does not have permissions" do
|
|
project_wo_permissions
|
|
visit "#{project_wo_permissions.path}/postgres/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - Forbidden")
|
|
expect(page.status_code).to eq(403)
|
|
expect(page).to have_content "Forbidden"
|
|
end
|
|
end
|
|
|
|
describe "show" do
|
|
it "can show PostgreSQL database details" do
|
|
pg
|
|
visit "#{project.path}/postgres"
|
|
|
|
expect(page.title).to eq("Ubicloud - PostgreSQL Databases")
|
|
expect(page).to have_content pg.name
|
|
|
|
click_link pg.name, href: "#{project.path}#{pg.path}"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{pg.name}")
|
|
expect(page).to have_content pg.name
|
|
expect(page).to have_content "Waiting for host to be ready..."
|
|
|
|
expect(Prog::Postgres::PostgresResourceNexus).to receive(:dns_zone).and_return(true)
|
|
page.refresh
|
|
expect(page).to have_content "#{pg.name}.#{pg.ubid}.postgres.ubicloud.com"
|
|
expect(page).to have_no_content "Waiting for host to be ready..."
|
|
end
|
|
|
|
it "does not show delete or edit options without the appropriate permissions" do
|
|
pg
|
|
visit "#{project.path}/postgres"
|
|
click_link pg.name, href: "#{project.path}#{pg.path}"
|
|
expect(page.title).to eq("Ubicloud - #{pg.name}")
|
|
expect(page.body).to include "metric-destination-password"
|
|
expect(page.body).to include "form-pg-md-create"
|
|
expect(page.body).not_to include "Lantern is a PostgreSQL-based vector database"
|
|
expect(page).to have_content "Danger Zone"
|
|
|
|
pg.this.update(flavor: PostgresResource::Flavor::LANTERN)
|
|
backup = Struct.new(:key, :last_modified)
|
|
restore_target = Time.now.utc
|
|
expect(MinioCluster).to receive(:[]).and_return(instance_double(MinioCluster, url: "dummy-url", root_certs: "dummy-certs")).at_least(:once)
|
|
expect(Minio::Client).to receive(:new).and_return(instance_double(Minio::Client, list_objects: [backup.new("basebackups_005/backup_stop_sentinel.json", restore_target - 10 * 60)])).at_least(:once)
|
|
page.refresh
|
|
fill_in "New server name", with: "restored-server"
|
|
fill_in "Target Time (UTC)", with: restore_target.strftime("%Y-%m-%d %H:%M"), visible: false
|
|
click_button "Fork"
|
|
expect(page.body).to include "metric-destination-password"
|
|
expect(page.body).to include "form-pg-md-create"
|
|
expect(page.body).to include "Lantern is a PostgreSQL-based vector database"
|
|
expect(page).to have_content "Danger Zone"
|
|
|
|
AccessControlEntry.dataset.destroy
|
|
AccessControlEntry.create(project_id: project.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:view"])
|
|
page.refresh
|
|
expect(page.title).to eq("Ubicloud - restored-server")
|
|
expect(page.body).not_to include "metric-destination-password"
|
|
expect(page.body).not_to include "form-pg-md-create"
|
|
expect(page.body).to include "Lantern is a PostgreSQL-based vector database"
|
|
expect(page).to have_no_content "Danger Zone"
|
|
end
|
|
|
|
it "raises forbidden when does not have permissions" do
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}"
|
|
|
|
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 PostgreSQL database not exists" do
|
|
visit "#{project.path}/location/eu-central-h1/postgres/08s56d4kaj94xsmrnf5v5m3mav"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
|
|
it "can restore PostgreSQL database" do
|
|
backup = Struct.new(:key, :last_modified)
|
|
restore_target = Time.now.utc
|
|
expect(MinioCluster).to receive(:[]).and_return(instance_double(MinioCluster, url: "dummy-url", root_certs: "dummy-certs")).at_least(:once)
|
|
expect(Minio::Client).to receive(:new).and_return(instance_double(Minio::Client, list_objects: [backup.new("basebackups_005/backup_stop_sentinel.json", restore_target - 10 * 60)])).at_least(:once)
|
|
|
|
visit "#{project.path}#{pg.path}"
|
|
expect(page).to have_content "Fork PostgreSQL database"
|
|
|
|
fill_in "New server name", with: "restored-server"
|
|
fill_in "Target Time (UTC)", with: restore_target.strftime("%Y-%m-%d %H:%M"), visible: false
|
|
|
|
click_button "Fork"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - restored-server")
|
|
end
|
|
|
|
it "can reset superuser password of PostgreSQL database" do
|
|
visit "#{project.path}#{pg.path}"
|
|
expect(page.title).to eq "Ubicloud - pg-with-permission"
|
|
expect(page).to have_content "Reset superuser password"
|
|
password = pg.superuser_password
|
|
|
|
find(".reset-superuser-password-new-password").set("Dummy")
|
|
find(".reset-superuser-password-new-password-repeat").set("DummyPassword123")
|
|
click_button "Reset"
|
|
expect(page).to have_flash_error "Validation failed for following fields: password, repeat_password"
|
|
expect(find_by_id("password-error").text).to eq "Password must have 12 characters minimum. Password must have at least one digit."
|
|
expect(find_by_id("repeat_password-error").text).to eq "Passwords must match."
|
|
|
|
expect(pg.reload.superuser_password).to eq password
|
|
|
|
find(".reset-superuser-password-new-password").set("DummyPassword123")
|
|
find(".reset-superuser-password-new-password-repeat").set("DummyPassword123")
|
|
click_button "Reset"
|
|
|
|
expect(page).to have_flash_notice "The superuser password will be updated in a few seconds"
|
|
expect(pg.reload.superuser_password).to eq("DummyPassword123")
|
|
expect(page.status_code).to eq(200)
|
|
end
|
|
|
|
it "does not show reset superuser password for restoring database" do
|
|
pg.representative_server.update(timeline_access: "fetch")
|
|
|
|
visit "#{project.path}#{pg.path}"
|
|
expect(page).to have_no_content "Reset superuser password"
|
|
expect(page.status_code).to eq(200)
|
|
end
|
|
|
|
it "cannot reset superuser password of restoring database" do
|
|
visit "#{project.path}#{pg.path}"
|
|
expect(page).to have_content "Reset superuser password"
|
|
|
|
pg.representative_server.update(timeline_access: "fetch")
|
|
find(".reset-superuser-password-new-password").set("DummyPassword123")
|
|
find(".reset-superuser-password-new-password-repeat").set("DummyPassword123")
|
|
click_button "Reset"
|
|
|
|
expect(page.status_code).to eq(400)
|
|
expect(page).to have_flash_error("Superuser password cannot be updated during restore!")
|
|
end
|
|
|
|
it "can restart PostgreSQL database" do
|
|
visit "#{project.path}#{pg.path}"
|
|
expect(page).to have_content "Restart"
|
|
click_button "Restart"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
end
|
|
end
|
|
|
|
describe "firewall" do
|
|
it "can show default firewall rules" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
expect(page).to have_content "Firewall Rules"
|
|
expect(page).to have_content "0.0.0.0/0"
|
|
expect(page).to have_content "5432"
|
|
end
|
|
|
|
it "can delete firewall rules" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
btn = find "#fwr-delete-#{pg.firewall_rules.first.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be true
|
|
end
|
|
|
|
it "can not delete firewall rules when does not have permissions" do
|
|
AccessControlEntry.create_with_id(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:view"])
|
|
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find "#fwr-delete-#{pg.firewall_rules.first.ubid} .delete-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "does not show create firewall rule when does not have permissions" do
|
|
AccessControlEntry.create_with_id(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:view"])
|
|
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find_by_id "fwr-create" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "can create firewall rule" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
fill_in "cidr", with: "1.1.1.2"
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_content "Firewall rule is created"
|
|
expect(page).to have_content "1.1.1.2/32"
|
|
expect(page).to have_content "5432"
|
|
|
|
fill_in "cidr", with: "12.12.12.0/26"
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_content "Firewall rule is created"
|
|
|
|
fill_in "cidr", with: "fd00::/64"
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_content "Firewall rule is created"
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_content "fd00::/64"
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be true
|
|
end
|
|
end
|
|
|
|
describe "metric-destination" do
|
|
it "can create metric destination" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
fill_in "url", with: "https://example.com"
|
|
fill_in "username", with: "username"
|
|
find(".metric-destination-password").set("password")
|
|
find(".metric-destination-create-button").click
|
|
expect(page).to have_content "https://example.com"
|
|
expect(pg.reload.metric_destinations.count).to eq(1)
|
|
end
|
|
|
|
it "can delete metric destinations" do
|
|
md = PostgresMetricDestination.create_with_id(
|
|
postgres_resource_id: pg.id,
|
|
url: "https://example.com",
|
|
username: "username",
|
|
password: "password"
|
|
)
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
btn = find "#md-delete-#{md.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(pg.reload.metric_destinations.count).to eq(0)
|
|
end
|
|
|
|
it "cannot delete metric destination if it is not exist" do
|
|
md = PostgresMetricDestination.create_with_id(
|
|
postgres_resource_id: pg.id,
|
|
url: "https://example.com",
|
|
username: "username",
|
|
password: "password"
|
|
)
|
|
expect(PostgresMetricDestination).to receive(:from_ubid).and_return(nil)
|
|
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
btn = find "#md-delete-#{md.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(pg.reload.metric_destinations.count).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe "delete" do
|
|
it "can delete PostgreSQL database" do
|
|
visit "#{project.path}#{pg.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 "#postgres-delete-#{pg.ubid} .delete-btn"
|
|
page.driver.delete btn["data-url"], {_csrf: btn["data-csrf"]}
|
|
|
|
expect(SemSnap.new(pg.id).set?("destroy")).to be true
|
|
end
|
|
|
|
it "can not delete PostgreSQL database when does not have permissions" do
|
|
# Give permission to view, so we can see the detail page
|
|
AccessControlEntry.create_with_id(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:view"])
|
|
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find ".delete-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
end
|
|
end
|
|
end
|