These requests cannot be submitted directly by browsers, only via javascript, and our javascript always uses JSON format for them. I'm fairly sure treating these as HTML requests, and showing an error HTML page, breaks the javascript code handling the request. Fix one case in the specs that expected a flash notice after javascript failure, which I'm guessing didn't actually work as desired in a browser executing javascript. This also allows removal of some no-longer-reachable code.
899 lines
36 KiB
Ruby
899 lines
36 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../spec_helper"
|
|
require "aws-sdk-s3"
|
|
|
|
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_id: Location::HETZNER_FSN1_ID,
|
|
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_id: Location::HETZNER_FSN1_ID,
|
|
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(name: "default")
|
|
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)
|
|
|
|
vmc = instance_double(VictoriaMetrics::Client, query_range: [nil])
|
|
vms = instance_double(VictoriaMetricsServer, client: vmc)
|
|
vmr = instance_double(VictoriaMetricsResource, servers: [vms])
|
|
allow(VictoriaMetricsResource).to receive(:first).and_return(vmr)
|
|
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
|
|
|
|
it "can list PostgreSQL databases with parents" do
|
|
pg
|
|
pg.update(parent_id: pg_wo_permission.id)
|
|
visit "#{project.path}/postgres"
|
|
|
|
expect(page).to have_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: Location::HETZNER_FSN1_UBID
|
|
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.project_id).to eq(project.id)
|
|
end
|
|
|
|
it "can create new PostgreSQL database in a custom AWS region" do
|
|
project
|
|
private_location = create_private_location(project: project)
|
|
Location.where(id: [Location::HETZNER_FSN1_ID, Location::LEASEWEB_WDC02_ID]).destroy
|
|
|
|
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: private_location.ubid
|
|
choose option: "m6id.large"
|
|
choose option: PostgresResource::HaType::NONE
|
|
choose option: "118"
|
|
|
|
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)
|
|
pg = PostgresResource.first
|
|
expect(pg.project_id).to eq(project.id)
|
|
expect(pg.target_storage_size_gib).to eq(118)
|
|
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: Location::HETZNER_FSN1_UBID
|
|
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("Invalid storage size. Available options: 1024, 2048, 4096")
|
|
expect(PostgresResource.count).to eq(0)
|
|
end
|
|
|
|
it "cannot create new PostgreSQL database with invalid location" 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: Location::HETZNER_FSN1_UBID
|
|
choose option: "standard-60"
|
|
choose option: PostgresResource::HaType::NONE
|
|
Location[Location::HETZNER_FSN1_ID].destroy
|
|
|
|
click_button "Create"
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
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: Location::HETZNER_FSN1_UBID
|
|
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.project_id).to eq(project.id)
|
|
end
|
|
|
|
it "can create new Lantern PostgreSQL database when the feature flag is enabled" do
|
|
project.set_ff_postgres_lantern(true)
|
|
visit "#{project.path}/postgres/create?flavor=#{PostgresResource::Flavor::LANTERN}"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create Lantern PostgreSQL Database")
|
|
name = "new-pg-db"
|
|
fill_in "Name", with: name
|
|
choose option: Location::HETZNER_FSN1_UBID
|
|
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.project_id).to eq(project.id)
|
|
end
|
|
|
|
it "can not create new ParadeDB PostgreSQL database in a customer specific location" do
|
|
project
|
|
private_location = create_private_location(project: project)
|
|
|
|
visit "#{project.path}/postgres/create?flavor=#{PostgresResource::Flavor::PARADEDB}"
|
|
|
|
expect(page.title).to eq("Ubicloud - Create ParadeDB PostgreSQL Database")
|
|
expect(page).to have_no_content private_location.name
|
|
end
|
|
|
|
it "treats invalid flavor as standard flavor when creating" 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 - Create PostgreSQL Database")
|
|
expect(find("input[name=flavor]", visible: false)[:value]).to eq "standard"
|
|
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: Location::HETZNER_FSN1_UBID
|
|
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("project_id and location_id and 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
|
|
|
|
it "cannot create when location not exist" do
|
|
visit "#{project.path}/location/not-exist-location/postgres/create"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
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}/overview"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{pg.name}")
|
|
expect(page).to have_content pg.name
|
|
end
|
|
|
|
it "can show PostgreSQL database details even when no subpage is specified" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}"
|
|
|
|
expect(page.title).to eq("Ubicloud - #{pg.name}")
|
|
expect(page).to have_content pg.name
|
|
end
|
|
|
|
it "can show disk usage details" do
|
|
pg
|
|
pg.representative_server.vm.add_vm_storage_volume(boot: false, size_gib: 128, disk_index: 0)
|
|
|
|
vmc = instance_double(VictoriaMetrics::Client, query_range: [{"values" => [[Time.now.utc.to_i, "50"]]}])
|
|
vms = instance_double(VictoriaMetricsServer, client: vmc)
|
|
vmr = instance_double(VictoriaMetricsResource, servers: [vms])
|
|
expect(VictoriaMetricsResource).to receive(:first).and_return(vmr)
|
|
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_content "64.0 GB is used (50.0%)"
|
|
end
|
|
|
|
it "shows the disk usage in red if usage is high" do
|
|
pg
|
|
pg.representative_server.vm.add_vm_storage_volume(boot: false, size_gib: 128, disk_index: 0)
|
|
|
|
vmc = instance_double(VictoriaMetrics::Client, query_range: [{"values" => [[Time.now.utc.to_i, "90"]]}])
|
|
vms = instance_double(VictoriaMetricsServer, client: vmc)
|
|
vmr = instance_double(VictoriaMetricsResource, servers: [vms])
|
|
expect(VictoriaMetricsResource).to receive(:first).and_return(vmr)
|
|
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_css("span.text-red-600", text: "115.2 GB is used (90.0%)")
|
|
end
|
|
|
|
it "shows total disk if there is no VictoriaMetricsResource" do
|
|
pg
|
|
pg.representative_server.vm.add_vm_storage_volume(boot: false, size_gib: 128, disk_index: 0)
|
|
|
|
expect(VictoriaMetricsResource).to receive(:first).and_return(nil)
|
|
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_content "128 GB"
|
|
end
|
|
|
|
it "shows AZ id for AWS PostgreSQL instance" do
|
|
AwsInstance.create_with_id(pg.representative_server.vm.id, instance_id: "i-0123456789abcdefg", az_id: "usw2-az2")
|
|
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_content "usw2-az2 (AWS)"
|
|
end
|
|
|
|
it "shows total disk if VictoriaMetricsResource is not accessible" do
|
|
pg
|
|
pg.representative_server.vm.add_vm_storage_volume(boot: false, size_gib: 128, disk_index: 0)
|
|
|
|
vmc = instance_double(VictoriaMetrics::Client)
|
|
expect(vmc).to receive(:query_range).and_raise(Excon::Error::Socket)
|
|
vms = instance_double(VictoriaMetricsServer, client: vmc)
|
|
vmr = instance_double(VictoriaMetricsResource, servers: [vms])
|
|
expect(VictoriaMetricsResource).to receive(:first).and_return(vmr)
|
|
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_content "128 GB"
|
|
end
|
|
|
|
it "can show basic metrics on overview page" do
|
|
pg.strand.update(label: "wait")
|
|
visit "#{project.path}#{pg.path}/overview"
|
|
expect(page).to have_css(".metric-chart")
|
|
end
|
|
|
|
it "shows connections if the resource is running" do
|
|
pg.strand.update(label: "wait")
|
|
visit "#{project.path}#{pg.path}/connection"
|
|
expect(page).to have_no_content "No connection information available"
|
|
end
|
|
|
|
it "does not show connections if the resource is creating" do
|
|
pg.strand.update(label: "wait_servers")
|
|
visit "#{project.path}#{pg.path}/connection"
|
|
expect(page).to have_content "No connection information available"
|
|
end
|
|
|
|
it "shows 404 for invalid pages for read replicas" do
|
|
pg
|
|
pg.update(parent_id: pg_wo_permission.id)
|
|
visit "#{project.path}#{pg.path}/resize"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
end
|
|
|
|
it "does not show delete or edit options without the appropriate permissions" do
|
|
pg
|
|
pg.timeline.update(cached_earliest_backup_at: Time.now.utc)
|
|
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
expect(page).to have_css(".firewall-rule-create-button")
|
|
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
expect(page).to have_css(".pg-read-replica-create-btn")
|
|
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
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"])
|
|
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
expect(page).to have_no_css(".firewall-rule-create-button")
|
|
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
expect(page).to have_no_css(".pg-read-replica-create-btn")
|
|
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
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}/overview"
|
|
|
|
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/overview"
|
|
|
|
expect(page.title).to eq("Ubicloud - ResourceNotFound")
|
|
expect(page.status_code).to eq(404)
|
|
expect(page).to have_content "ResourceNotFound"
|
|
end
|
|
|
|
it "can update PostgreSQL instance size configuration" do
|
|
pg.representative_server.vm.add_vm_storage_volume(boot: false, size_gib: 128, disk_index: 0)
|
|
|
|
visit "#{project.path}#{pg.path}/resize"
|
|
|
|
choose option: "standard-8"
|
|
choose option: 256
|
|
|
|
# We send PATCH request manually instead of just clicking to button because PATCH action triggered by JavaScript.
|
|
# UI tests run without a JavaScript engine.
|
|
form = find_by_id "creation-form"
|
|
_csrf = form.find("input[name='_csrf']", visible: false).value
|
|
size = form.find(:radio_button, "size", checked: true).value
|
|
storage_size = form.find(:radio_button, "storage_size", checked: true).value
|
|
page.driver.submit :patch, form["action"], {size:, storage_size:, _csrf:}
|
|
|
|
pg.reload
|
|
expect(pg.target_vm_size).to eq("standard-8")
|
|
expect(pg.target_storage_size_gib).to eq(256)
|
|
end
|
|
|
|
it "handles errors during scale up/down" do
|
|
visit "#{project.path}#{pg.path}/resize"
|
|
|
|
choose option: "standard-8"
|
|
choose option: 64
|
|
|
|
# We send PATCH request manually instead of just clicking to button because PATCH action triggered by JavaScript.
|
|
# UI tests run without a JavaScript engine.
|
|
form = find_by_id "creation-form"
|
|
_csrf = form.find("input[name='_csrf']", visible: false).value
|
|
size = form.find(:radio_button, "size", checked: true).value
|
|
storage_size = form.find(:radio_button, "storage_size", checked: true).value
|
|
page.driver.submit :patch, form["action"], {size:, storage_size:, _csrf:}
|
|
|
|
# Error messages are displayed to the user via javascript, using the error.message entry
|
|
expect(JSON.parse(page.driver.browser.last_response.body).dig("error", "message")).to eq "Validation failed for following fields: storage_size"
|
|
|
|
pg.reload
|
|
expect(pg.target_vm_size).to eq("standard-2")
|
|
expect(pg.target_storage_size_gib).to eq(128)
|
|
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}/backup-restore"
|
|
expect(page).to have_content "Fork PostgreSQL database"
|
|
fill_in "#{pg.name}-fork", with: "restored-server"
|
|
click_button "Fork"
|
|
expect(page.title).to eq("Ubicloud - pg-with-permission")
|
|
expect(page).to have_flash_error("empty string provided for parameter restore_target")
|
|
|
|
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")
|
|
expect(page).to have_flash_notice("'restored-server' will be ready in a few minutes")
|
|
end
|
|
|
|
it "shows proper message when there is no backups to restore" do
|
|
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: [])).at_least(:once)
|
|
|
|
visit "#{project.path}#{pg.path}/backup-restore"
|
|
expect(page).to have_content "No backups available for this PostgreSQL database."
|
|
end
|
|
|
|
it "can create a read replica of a PostgreSQL database" do
|
|
pg.timeline.update(cached_earliest_backup_at: Time.now.utc)
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
|
|
fill_in "#{pg.name}-read-replica", with: "my-read-replica"
|
|
|
|
find(".pg-read-replica-create-btn").click
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - my-read-replica")
|
|
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
expect(page).to have_content("my-read-replica")
|
|
end
|
|
|
|
it "cannot create a read replica if there is no backup, yet" do
|
|
pg.timeline.update(cached_earliest_backup_at: Time.now.utc)
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
pg.timeline.update(cached_earliest_backup_at: nil)
|
|
|
|
fill_in "#{pg.name}-read-replica", with: "my-read-replica"
|
|
|
|
find(".pg-read-replica-create-btn").click
|
|
|
|
expect(page.status_code).to eq(400)
|
|
expect(page).to have_content("Parent server is not ready for read replicas. There are no backups, yet.")
|
|
end
|
|
|
|
it "can promote a read replica" do
|
|
pg.timeline.update(cached_earliest_backup_at: Time.now.utc)
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
|
|
fill_in "#{pg.name}-read-replica", with: "my-read-replica"
|
|
|
|
find(".pg-read-replica-create-btn").click
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - my-read-replica")
|
|
|
|
visit "#{project.path}#{pg.read_replicas.first.path}/settings"
|
|
find(".promote-btn").click
|
|
expect(PostgresResource[name: "my-read-replica"].semaphores.count).to eq(1)
|
|
expect(page).to have_content "'my-read-replica' will be promoted in a few minutes, please refresh the page"
|
|
end
|
|
|
|
it "fails to promote if not a read replica" do
|
|
pg.timeline.update(cached_earliest_backup_at: Time.now.utc)
|
|
visit "#{project.path}#{pg.path}/read-replica"
|
|
expect(page).to have_content "Read Replicas"
|
|
|
|
fill_in "#{pg.name}-read-replica", with: "my-read-replica"
|
|
|
|
find(".pg-read-replica-create-btn").click
|
|
|
|
expect(page.status_code).to eq(200)
|
|
expect(page.title).to eq("Ubicloud - my-read-replica")
|
|
|
|
pg_read_replica = PostgresResource[name: "my-read-replica"]
|
|
visit "#{project.path}#{pg_read_replica.path}/settings"
|
|
PostgresResource[name: "my-read-replica"].update(parent_id: nil)
|
|
find(".promote-btn").click
|
|
expect(page.status_code).to eq(400)
|
|
expect(page).to have_flash_error("Non read replica servers cannot be promoted.")
|
|
end
|
|
|
|
it "can reset superuser password of PostgreSQL database" do
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
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 "can restart PostgreSQL database" do
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
expect(page).to have_content "Restart"
|
|
click_button "Restart"
|
|
|
|
expect(page.status_code).to eq(200)
|
|
end
|
|
|
|
it "doesn't show reset button 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["Postgres:view"])
|
|
AccessControlEntry.create(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:delete"])
|
|
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}/settings"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find ".restart-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "shows metrics if the resource is not creating" do
|
|
pg.strand.update(label: "wait")
|
|
visit "#{project.path}#{pg.path}/charts"
|
|
expect(page).to have_content "CPU Usage"
|
|
end
|
|
|
|
it "does not show metrics the resource is creating" do
|
|
pg.strand.update(label: "wait_servers")
|
|
visit "#{project.path}#{pg.path}/charts"
|
|
expect(page).to have_no_content "CPU Usage"
|
|
end
|
|
end
|
|
|
|
describe "firewall" do
|
|
it "can show default firewall rules" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
|
|
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}/networking"
|
|
|
|
btn = find "#fwr-buttons-#{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(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}/networking"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find "#fwr-buttons-#{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(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}/networking"
|
|
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
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_flash_error "empty string provided for parameter cidr"
|
|
|
|
find('input[name="cidr"][form="form-pg-fwr-create"]').set("1.1.1.2")
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_flash_notice "Firewall rule is created"
|
|
expect(page).to have_content "1.1.1.2/32"
|
|
expect(page).to have_content "5432"
|
|
|
|
find('input[name="cidr"][form="form-pg-fwr-create"]').set("12.12.12.0/26")
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_flash_notice "Firewall rule is created"
|
|
|
|
find('input[name="cidr"][form="form-pg-fwr-create"]').set("fd00::/64")
|
|
find('input[name="description"][form="form-pg-fwr-create"]').set("test description - new firewall rule")
|
|
find(".firewall-rule-create-button").click
|
|
expect(page).to have_flash_notice "Firewall rule is created"
|
|
expect(page.status_code).to eq(200)
|
|
expect(page).to have_content "fd00::/64"
|
|
expect(page).to have_content "test description - new firewall rule"
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be true
|
|
end
|
|
|
|
it "can update firewall rule" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
|
|
btn = find "#fwr-buttons-#{pg.firewall_rules.first.ubid} .save-inline-btn"
|
|
url = btn["data-url"]
|
|
_csrf = btn["data-csrf"]
|
|
page.driver.submit :patch, url, {cidr: "0.0.0.0/1", description: "dummy-description", _csrf:}
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be true
|
|
end
|
|
|
|
it "can set nil description for firewall rule" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
|
|
btn = find "#fwr-buttons-#{pg.firewall_rules.first.ubid} .save-inline-btn"
|
|
url = btn["data-url"]
|
|
_csrf = btn["data-csrf"]
|
|
page.driver.submit :patch, url, {cidr: "0.0.0.0/1", description: nil, _csrf:}
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be true
|
|
end
|
|
|
|
it "doesn't increment update_firewall_rules semaphore if cidr is same" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
|
|
btn = find "#fwr-buttons-#{pg.firewall_rules.first.ubid} .save-inline-btn"
|
|
url = btn["data-url"]
|
|
_csrf = btn["data-csrf"]
|
|
page.driver.submit :patch, url, {cidr: "0.0.0.0/0", description: "test", _csrf:}
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).to be false
|
|
end
|
|
|
|
it "cannot delete firewall rule if it doesn't exist" do
|
|
pg
|
|
visit "#{project.path}#{pg.path}/networking"
|
|
|
|
btn = find "#fwr-buttons-#{pg.firewall_rules.first.ubid} .save-inline-btn"
|
|
url = btn["data-url"]
|
|
_csrf = btn["data-csrf"]
|
|
|
|
fwr = pg.firewall_rules.first
|
|
fwr.update(cidr: "0.0.0.0/1", postgres_resource_id: pg_wo_permission.id)
|
|
|
|
page.driver.submit :patch, url, {cidr: "0.0.0.0/2", description: "dummy-description", _csrf:}
|
|
|
|
expect(SemSnap.new(pg.id).set?("update_firewall_rules")).not_to be true
|
|
end
|
|
end
|
|
|
|
describe "metric-destination" do
|
|
it "can create metric destination" do
|
|
visit "#{project.path}#{pg.path}/charts"
|
|
find(".metric-destination-create-button").click
|
|
fill_in "username", with: "username"
|
|
expect(page).to have_flash_error "empty string provided for parameter url"
|
|
|
|
fill_in "url", with: "https://example.com"
|
|
find(".metric-destination-password").set("password")
|
|
find(".metric-destination-create-button").click
|
|
expect(page.title).to eq "Ubicloud - pg-with-permission"
|
|
expect(page).to have_flash_notice "Metric destination is created"
|
|
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(
|
|
postgres_resource_id: pg.id,
|
|
url: "https://example.com",
|
|
username: "username",
|
|
password: "password"
|
|
)
|
|
visit "#{project.path}#{pg.path}/charts"
|
|
|
|
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(
|
|
postgres_resource_id: pg.id,
|
|
url: "https://example.com",
|
|
username: "username",
|
|
password: "password"
|
|
)
|
|
|
|
visit "#{project.path}#{pg.path}/charts"
|
|
md.this.update(id: PostgresMetricDestination.generate_uuid)
|
|
|
|
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 "set-maintenance-window" do
|
|
it "sets maintenance window to nil when empty string is passed" do
|
|
pg.update(maintenance_window_start_at: 9)
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
|
|
select "No Maintenance Window", from: "maintenance_window_start_at"
|
|
click_button "Set"
|
|
expect(pg.reload.maintenance_window_start_at).to be_nil
|
|
end
|
|
|
|
it "sets maintenance window to 0 when 0 is passed" do
|
|
pg.update(maintenance_window_start_at: 9)
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
|
|
select "00:00 - 02:00 (UTC)", from: "maintenance_window_start_at"
|
|
click_button "Set"
|
|
expect(pg.reload.maintenance_window_start_at).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "delete" do
|
|
it "can delete PostgreSQL database" do
|
|
visit "#{project.path}#{pg.path}/settings"
|
|
|
|
# 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(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:view"])
|
|
AccessControlEntry.create(project_id: project_wo_permissions.id, subject_id: user.id, action_id: ActionType::NAME_MAP["Postgres:edit"])
|
|
|
|
visit "#{project_wo_permissions.path}#{pg_wo_permission.path}/settings"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find ".delete-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
end
|
|
|
|
describe "config" do
|
|
it "can view configuration" do
|
|
pg.update(user_config: {"max_connections" => "120"})
|
|
visit "#{project.path}#{pg.path}/config"
|
|
|
|
expect(page).to have_content "PostgreSQL Configuration"
|
|
expect(page).to have_field "pg_config_keys[]", with: "max_connections"
|
|
expect(page).to have_field "pg_config_values[]", with: "120"
|
|
end
|
|
|
|
it "does not show update button when user does not have permissions" do
|
|
pg_wo_permission.update(user_config: {"max_connections" => "120"})
|
|
AccessControlEntry.create(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}/config"
|
|
expect(page.title).to eq "Ubicloud - pg-without-permission"
|
|
|
|
expect { find ".delete-config-btn" }.to raise_error Capybara::ElementNotFound
|
|
expect { find ".save-config-btn" }.to raise_error Capybara::ElementNotFound
|
|
end
|
|
|
|
it "shows update button when user has permissions" do
|
|
visit "#{project.path}#{pg.path}/config"
|
|
expect(page).to have_button "Save"
|
|
|
|
expect { find ".pg-config-card .delete-config-btn" }.not_to raise_error
|
|
expect { find ".save-config-btn" }.not_to raise_error
|
|
end
|
|
|
|
it "can update configuration" do
|
|
pg
|
|
pg.update(user_config: {"max_connections" => "120"})
|
|
visit "#{project.path}#{pg.path}/config"
|
|
|
|
within ".pg-config-card .new-config" do
|
|
fill_in "pg_config_keys[]", with: "max_connections"
|
|
fill_in "pg_config_values[]", with: "240"
|
|
end
|
|
click_button "Save"
|
|
|
|
expect(page).to have_field "pg_config_keys[]", with: "max_connections"
|
|
expect(page).to have_field "pg_config_values[]", with: "240"
|
|
expect(page).to have_flash_notice "Configuration updated successfully"
|
|
expect(pg.reload.user_config).to eq({"max_connections" => "240"})
|
|
end
|
|
|
|
it "shows errors when an unknown configuration is provided" do
|
|
pg.update(user_config: {"max_connections" => "120"})
|
|
visit "#{project.path}#{pg.path}/config"
|
|
|
|
within ".pg-config-card .new-config" do
|
|
fill_in "pg_config_keys[]", with: "invalid"
|
|
fill_in "pg_config_values[]", with: "invalid"
|
|
end
|
|
click_button "Save"
|
|
|
|
expect(page).to have_content "Unknown configuration parameter"
|
|
expect(page).to have_flash_error "Validation failed for following fields: pg_config.invalid"
|
|
expect(pg.reload.user_config).to eq({"max_connections" => "120"})
|
|
end
|
|
|
|
it "shows errors when an invalid configuration is provided" do
|
|
pg.update(user_config: {"max_connections" => "120"})
|
|
visit "#{project.path}#{pg.path}/config"
|
|
|
|
within ".pg-config-card .new-config" do
|
|
fill_in "pg_config_keys[]", with: "work_mem"
|
|
fill_in "pg_config_values[]", with: "16iB"
|
|
end
|
|
click_button "Save"
|
|
|
|
expect(page).to have_flash_error "Validation failed for following fields: pg_config.work_mem"
|
|
expect(page).to have_content "must match pattern: ^[0-9]+(kB|MB|GB|TB)?$"
|
|
expect(pg.reload.user_config).to eq({"max_connections" => "120"})
|
|
end
|
|
end
|
|
end
|
|
end
|