Files
ubicloud/spec/ubid_spec.rb
Enes Cakir 8eb223790a Raise a parse error for all types of invalid UBID
If the UBID is not 26 characters long, we raise a UBIDParseError
exception. In `Model.from_ubid`, if the UBIDParseError exception is
raised, we catch it and return nil since the resource does not exist.

However, a UBID can be 26 characters long but still not valid. For
example, "vm164pbm96-a3grq6vbsvjb3ax" has 26 characters, but because of
the "-", it's not a valid UBID. When an invalid UBID with 26 characters
is passed to the URL, it raises an "Invalid base32 encoding" error and
returns an HTTP 500 exception which happens in production time to time.
Instead, we should return HTTP 404 since the resource with the given ID
does not exist. We return 404 if the UBID doesn't have 26 characters.
2025-02-20 09:57:53 +03:00

370 lines
16 KiB
Ruby

# frozen_string_literal: true
RSpec.describe UBID do
let(:all_types) { described_class.constants.select { _1.start_with?("TYPE_") }.map { described_class.const_get(_1) } }
it ".generate_vanity_action_type supports creating vanity ubids for action types" do
expect(described_class.generate_vanity_action_type("Project:view").to_s).to eq "ttzzzzzzzz021gzzz0pj0v1ew0"
expect(described_class.generate_vanity_action_type("ObjectTag:add").to_s).to eq "ttzzzzzzzz021gzzzz0t00add0"
end
it ".generate_vanity_action_tag supports creating vanity ubids for action tags" do
expect(described_class.generate_vanity_action_tag("Vm:all").to_s).to eq "tazzzzzzzz021gzzzz0vm0a111"
expect(described_class.generate_vanity_action_tag("Member").to_s).to eq "tazzzzzzzz021gzzzz0member0"
end
it ".generate_vanity raises if prefix or suffix is too long" do
expect { described_class.generate_vanity("tt", "foo", "bar") }.to raise_error(RuntimeError)
expect { described_class.generate_vanity("tt", "fo", "barbazqu") }.to raise_error(RuntimeError)
end
it "can set_bits" do
expect(described_class.set_bits(0, 0, 7, 0xab)).to eq(0xab)
expect(described_class.set_bits(0xab, 8, 12, 0xc)).to eq(0xcab)
expect(described_class.set_bits(0xcab, 40, 48, 0x12)).to eq(0x120000000cab)
end
it "can get bits" do
expect(described_class.get_bits(0x120000000cab, 8, 11)).to eq(0xc)
end
it "can convert to base32" do
tests = [
["0", 0], ["o", 0], ["A", 10], ["e", 14], ["m", 20], ["S", 25], ["z", 31]
]
tests.each {
expect(described_class.to_base32(_1[0])).to eq(_1[1])
}
end
it "can extract bits as hex" do
expect(described_class.extract_bits_as_hex(0x12345, 1, 3)).to eq("234")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 4, 5)).to eq("cdef0")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 1, 4)).to eq("0001")
expect(described_class.extract_bits_as_hex(0xabcdef00012, 9, 5)).to eq("000ab")
end
it "can encode multiple numbers" do
expect(described_class.from_base32_n(10 + 20 * 32 + 1 * 32 * 32, 3)).to eq("1ma")
end
it "fails to convert U to base32" do
expect { described_class.to_base32("u") }.to raise_error UBIDParseError, "Invalid base32 encoding: U"
end
it "can convert from base32" do
tests = [
[0, "0"], [12, "c"], [16, "g"], [20, "m"], [26, "t"], [28, "w"], [31, "z"]
]
tests.each {
expect(described_class.from_base32(_1[0])).to eq(_1[1])
}
end
it "can convert multiple from base32" do
expect(described_class.to_base32_n("1MA")).to eq(10 + 20 * 32 + 1 * 32 * 32)
end
it "fails to convert from out of range numbers" do
expect {
described_class.from_base32(-1)
}.to raise_error UBIDParseError, "Invalid base32 number: -1"
expect {
described_class.from_base32(32)
}.to raise_error UBIDParseError, "Invalid base32 number: 32"
end
it "can calculate parity" do
expect(described_class.parity(0b10101)).to eq(1)
expect(described_class.parity(0b101011)).to eq(0)
end
it "can create from parts & encode it & parse it" do
id = described_class.from_parts(1000, "ab", 3, 12292929)
expect(described_class.parse(id.to_s).to_i).to eq(id.to_i)
end
it "can generate random ids" do
ubid1 = described_class.generate(UBID::TYPE_VM)
ubid2 = described_class.parse(ubid1.to_s)
expect(ubid2.to_i).to eq(ubid1.to_i)
end
it "can convert to and from uuid" do
ubid1 = described_class.generate(UBID::TYPE_VM)
ubid2 = described_class.from_uuidish(ubid1.to_uuid)
expect(ubid2.to_s).to eq(ubid1.to_s)
end
it "can convert from and to uuid" do
uuid1 = "550e8400-e29b-41d4-a716-446655440000"
uuid2 = described_class.from_uuidish(uuid1).to_uuid
expect(uuid2).to eq(uuid1)
end
it "fails to parse if length is not 26" do
expect {
described_class.parse("123456")
}.to raise_error UBIDParseError, "Invalid encoding length: 6"
end
it "fails to parse if length is 26 but has invalid characters" do
expect {
described_class.parse("vm164pbm96-a3grq6vbsvjb3ax")
}.to raise_error UBIDParseError, "Invalid base32 encoding: -"
end
it "fails to parse if top bits parity is incorrect" do
invalid_ubid = "vm164pbm96ba3grq6vbsvjb3ax"
expect {
described_class.parse(invalid_ubid)
}.to raise_error UBIDParseError, "Invalid top bits parity"
end
it "fails to parse if bottom bits parity is incorrect" do
invalid_ubid = "vm064pbm96ba3grq6vbsvjb3az"
expect {
described_class.parse(invalid_ubid)
}.to raise_error UBIDParseError, "Invalid bottom bits parity"
end
it "generates random timestamp for most types" do
(all_types - described_class::CURRENT_TIMESTAMP_TYPES).each { |type|
# generate 10 ids with this type, and verify that their timestamp
# part is not close
ubids = (1..10).map { described_class.generate(type).to_i }
max_ubid = ubids.max
min_ubid = ubids.min
# Timestamp is the left 48 bits of the 128-bit ubid.
time_difference = (max_ubid >> 80) - (min_ubid >> 80)
# Timestamp parts are random, so with a very high probability
# the difference between them will be > 8ms.
expect(time_difference).to be > 8
}
end
it "generates clock timestamp for strand and semaphore" do
described_class::CURRENT_TIMESTAMP_TYPES.each { |type|
# generate 10 ids with this type, and verify that their timestamp
# part is close
ubids = (1..10).map { described_class.generate(type).to_i }
max_ubid = ubids.max
min_ubid = ubids.min
# Timestamp is the left 48 bits of the 128-bit ubid.
time_difference = (max_ubid >> 80) - (min_ubid >> 80)
# Timestamp parts are sequential, so with a very high probability
# the difference between them will be < 128ms.
expect(time_difference).to be < 128
}
end
it "uses canonical type characters" do
# In crockford's base32 multiple characters can map to a single number,
# for example ["1","I","L"] all map to 1. However, the reverse (number
# -> character) only uses one canonical one (e.g. 1 maps to "1").
# This test ensures we only use canonical characters in our type
# constants, so ids are actually prefixed by those constants.
#
# See https://www.crockford.com/base32.html
all_types.each { |type|
ubid = described_class.generate(type).to_s
expect(ubid).to start_with type
}
end
it "has unique type identifiers" do
expect(all_types.uniq.length).to eq(all_types.length)
end
it "generates ids with proper prefix" do
sshable = Sshable.create_with_id
expect(sshable.ubid).to start_with UBID::TYPE_SSHABLE
host = create_vm_host
si = SpdkInstallation.create(version: "v1", allocation_weight: 100, vm_host_id: host.id) { _1.id = host.id }
vm = create_vm
expect(vm.ubid).to start_with UBID::TYPE_VM
dev = StorageDevice.create(name: "x", available_storage_gib: 1, total_storage_gib: 1, vm_host_id: host.id) { _1.id = host.id }
sv = VmStorageVolume.create_with_id(vm_id: vm.id, size_gib: 5, disk_index: 0, boot: false, spdk_installation_id: si.id, storage_device_id: dev.id)
expect(sv.ubid).to start_with UBID::TYPE_VM_STORAGE_VOLUME
kek = StorageKeyEncryptionKey.create_with_id(algorithm: "x", key: "x", init_vector: "x", auth_data: "x")
expect(kek.ubid).to start_with UBID::TYPE_STORAGE_KEY_ENCRYPTION_KEY
account = Account.create_with_id(email: "x@y.net")
expect(account.ubid).to start_with UBID::TYPE_ACCOUNT
prj = account.create_project_with_default_policy("x")
expect(prj.ubid).to start_with UBID::TYPE_PROJECT
st = SubjectTag.create_with_id(project_id: prj.id, name: "T")
expect(st.ubid).to start_with UBID::TYPE_SUBJECT_TAG
at = ActionTag.create_with_id(project_id: prj.id, name: "T")
expect(at.ubid).to start_with UBID::TYPE_ACTION_TAG
ot = ObjectTag.create_with_id(project_id: prj.id, name: "T")
expect(ot.ubid).to start_with UBID::TYPE_OBJECT_TAG
ace = AccessControlEntry.create_with_id(project_id: prj.id, subject_id: st.id)
expect(ace.ubid).to start_with UBID::TYPE_ACCESS_CONTROL_ENTRY
expect(ActionType.first.ubid).to start_with UBID::TYPE_ACTION_TYPE
subnet = PrivateSubnet.create_with_id(net6: "0::0", net4: "127.0.0.1", name: "x", location: "x", project_id: prj.id)
expect(subnet.ubid).to start_with UBID::TYPE_PRIVATE_SUBNET
nic = Nic.create_with_id(
private_ipv6: "fd10:9b0b:6b4b:8fbb::/128",
private_ipv4: "10.0.0.12/32",
mac: "00:11:22:33:44:55",
encryption_key: "0x30613961313636632d653765372d343434372d616232392d376561343432623562623065",
private_subnet_id: subnet.id,
name: "def-nic"
)
expect(nic.ubid).to start_with UBID::TYPE_NIC
tun = IpsecTunnel.create_with_id(src_nic_id: nic.id, dst_nic_id: nic.id)
expect(tun.ubid).to start_with UBID::TYPE_IPSEC_TUNNEL
adr = Address.create_with_id(cidr: "192.168.1.0/24", routed_to_host_id: host.id)
expect(adr.ubid).to start_with UBID::TYPE_ADDRESS
vm_adr = AssignedVmAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, dst_vm_id: vm.id)
expect(vm_adr.ubid).to start_with UBID::TYPE_ASSIGNED_VM_ADDRESS
host_adr = AssignedHostAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, host_id: host.id)
expect(host_adr.ubid).to start_with UBID::TYPE_ASSIGNED_HOST_ADDRESS
strand = Strand.create_with_id(prog: "x", label: "y")
expect(strand.ubid).to start_with UBID::TYPE_STRAND
semaphore = Semaphore.create_with_id(strand_id: strand.id, name: "z")
expect(semaphore.ubid).to start_with UBID::TYPE_SEMAPHORE
page = Page.create_with_id(summary: "x", tag: "y")
expect(page.ubid).to start_with UBID::TYPE_PAGE
end
# useful for comparing objects having network values
def string_kv(obj)
obj.to_hash.map { |k, v| [k.to_s, v.to_s] }.to_h
end
it "can decode ids" do
sshable = Sshable.create_with_id
host = create_vm_host
si = SpdkInstallation.create(version: "v1", allocation_weight: 100, vm_host_id: host.id) { _1.id = host.id }
vm = create_vm
dev = StorageDevice.create(name: "x", available_storage_gib: 1, total_storage_gib: 1, vm_host_id: host.id) { _1.id = host.id }
sv = VmStorageVolume.create_with_id(vm_id: vm.id, size_gib: 5, disk_index: 0, boot: false, spdk_installation_id: si.id, storage_device_id: dev.id)
kek = StorageKeyEncryptionKey.create_with_id(algorithm: "x", key: "x", init_vector: "x", auth_data: "x")
account = Account.create_with_id(email: "x@y.net")
project = account.create_project_with_default_policy("x")
project_id = project.id
st = SubjectTag.create_with_id(project_id:, name: "T")
at = ActionTag.create_with_id(project_id:, name: "T")
ot = ObjectTag.create_with_id(project_id:, name: "T")
ace = AccessControlEntry.create_with_id(project_id:, subject_id: st.id)
a_type = ActionType.first
subnet = PrivateSubnet.create_with_id(net6: "0::0", net4: "127.0.0.1", name: "x", location: "x", project_id:)
nic = Nic.create_with_id(private_ipv6: "fd10:9b0b:6b4b:8fbb::/128", private_ipv4: "10.0.0.12/32", mac: "00:11:22:33:44:55", encryption_key: "0x30613961313636632d653765372d343434372d616232392d376561343432623562623065", private_subnet_id: subnet.id, name: "def-nic")
tun = IpsecTunnel.create_with_id(src_nic_id: nic.id, dst_nic_id: nic.id)
adr = Address.create_with_id(cidr: "192.168.1.0/24", routed_to_host_id: host.id)
vm_adr = AssignedVmAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, dst_vm_id: vm.id)
host_adr = AssignedHostAddress.create_with_id(ip: "192.168.1.1", address_id: adr.id, host_id: host.id)
strand = Strand.create_with_id(prog: "x", label: "y")
semaphore = Semaphore.create_with_id(strand_id: strand.id, name: "z")
page = Page.create_with_id(summary: "x", tag: "y")
expect(described_class.decode(vm.ubid)).to eq(vm)
expect(described_class.decode(sv.ubid)).to eq(sv)
expect(described_class.decode(kek.ubid)).to eq(kek)
expect(described_class.decode(account.ubid)).to eq(account)
expect(described_class.decode(project.ubid)).to eq(project)
expect(described_class.decode(st.ubid)).to eq(st)
expect(described_class.decode(at.ubid)).to eq(at)
expect(described_class.decode(ot.ubid)).to eq(ot)
expect(described_class.decode(ace.ubid)).to eq(ace)
expect(described_class.decode(a_type.ubid)).to eq(a_type)
expect(described_class.decode(tun.ubid)).to eq(tun)
expect(string_kv(described_class.decode(subnet.ubid))).to eq(string_kv(subnet))
expect(described_class.decode(sshable.ubid)).to eq(sshable)
expect(string_kv(described_class.decode(adr.ubid))).to eq(string_kv(adr))
expect(string_kv(described_class.decode(vm_adr.ubid))).to eq(string_kv(vm_adr))
expect(string_kv(described_class.decode(host_adr.ubid))).to eq(string_kv(host_adr))
expect(described_class.decode(strand.ubid)).to eq(strand)
expect(described_class.decode(semaphore.ubid)).to eq(semaphore)
expect(described_class.decode(page.ubid)).to eq(page)
expect(string_kv(described_class.decode(nic.ubid))).to eq(string_kv(nic))
end
it "fails to decode unknown type" do
expect {
described_class.decode("han2sefsk4f61k91z77vn0y978")
}.to raise_error UBIDParseError, "Couldn't decode ubid: han2sefsk4f61k91z77vn0y978"
end
it "can be inspected" do
expect(described_class.parse("vmqsknkzw5164hkfnt6z6zgjps").inspect).to eq("#<UBID:Vm @ubid=\"vmqsknkzw5164hkfnt6z6zgjps\" @uuid=\"be6759ff-8509-8b74-8cdf-5d1be6fc256c\">")
end
it ".resolve_map populates hash with uuid keys" do
page = Page.create_with_id(summary: "x", tag: "y")
project = Project.create(name: "test")
a_type = ActionType.first
api_key = ApiKey.create(owner_table: "project", owner_id: project.id, used_for: "inference_endpoint", key: "1", project_id: project.id)
invalid = described_class.to_uuid("han2sefsk4f61k91z77vn0y978")
hash = {page.id => nil, a_type.id => nil, api_key.id => nil, invalid => nil}
described_class.resolve_map(hash)
expect(hash[page.id]).to eq page
expect(hash[a_type.id]).to eq a_type
expect(hash[api_key.id]).to eq api_key
expect(hash[invalid]).to be_nil
end
it ".type_match? checks whether given ubid has given type" do
ubid = ActionType.first.ubid
expect(described_class.type_match?(ubid, described_class::TYPE_ACTION_TYPE)).to be true
expect(described_class.type_match?(ubid, described_class::TYPE_ETC)).to be false
end
it ".uuid_type_match? checks whether given uuid has given type" do
uuid = ActionType.first.id
expect(described_class.uuid_type_match?(uuid, described_class::TYPE_ACTION_TYPE)).to be true
expect(described_class.uuid_type_match?(uuid, described_class::TYPE_ETC)).to be false
end
it "#type_match? checks whether receiver has given type" do
ubid = described_class.from_uuidish(ActionType.first.id)
expect(ubid.type_match?(described_class::TYPE_ACTION_TYPE)).to be true
expect(ubid.type_match?(described_class::TYPE_ETC)).to be false
end
it ".class_match? checks whether given ubid has given class" do
ubid = ActionType.first.ubid
expect(described_class.class_match?(ubid, ActionType)).to be true
expect(described_class.class_match?(ubid, ApiKey)).to be false
end
it ".uuid_class_match? checks whether given uuid has given class" do
uuid = ActionType.first.id
expect(described_class.uuid_class_match?(uuid, ActionType)).to be true
expect(described_class.uuid_class_match?(uuid, ApiKey)).to be false
end
it "#class_match? checks whether receiver has given class" do
ubid = described_class.from_uuidish(ActionType.first.id)
expect(ubid.class_match?(ActionType)).to be true
expect(ubid.class_match?(ApiKey)).to be false
end
end