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.
370 lines
16 KiB
Ruby
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
|