Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions lib/structs/beacons.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ defmodule ExPass.Structs.Beacons do

* `:major` - 16-bit unsigned integer. The major identifier of a Bluetooth Low Energy location beacon.
* `:minor` - 16-bit unsigned integer. The minor identifier of a Bluetooth Low Energy location beacon.
* `:proximityUUID` - (Required) The unique identifier of a Bluetooth Low Energy location beacon.
* `:proximity_UUID` - (Required) The unique identifier of a Bluetooth Low Energy location beacon.
* `:relevant_text` - (Optional) The text to display on the lock screen when the pass is relevant.

## Compatibility

Expand All @@ -23,7 +24,8 @@ defmodule ExPass.Structs.Beacons do
typedstruct do
field :major, integer()
field :minor, integer()
field :proximityUUID, String.t(), enforce: true
field :proximity_UUID, String.t(), enforce: true
field :relevant_text, String.t()
end

@doc """
Expand All @@ -34,22 +36,32 @@ defmodule ExPass.Structs.Beacons do
* `attrs` - A map of attributes for the Beacons struct. The map can include the following keys:
* `:major` - (Optional) The major identifier of a Bluetooth Low Energy location beacon.
* `:minor` - (Optional) The minor identifier of a Bluetooth Low Energy location beacon.
* `:proximityUUID` - (Required) The unique identifier of a Bluetooth Low Energy location beacon.
* `:proximity_UUID` - (Required) The unique identifier of a Bluetooth Low Energy location beacon.
* `:relevant_text` - (Optional) The text to display on the lock screen when the pass is relevant.

## Returns

* A new `%Beacons{}` struct.

## Examples

iex> Beacons.new(%{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: 6_789})
%Beacons{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: 6_789}
iex> Beacons.new(%{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: 6_789})
%Beacons{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: 6_789, relevant_text: nil}

iex> Beacons.new(%{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345})
%Beacons{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: nil}
iex> Beacons.new(%{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345})
%Beacons{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 12_345, minor: nil, relevant_text: nil}

iex> Beacons.new(%{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"})
%Beacons{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: nil, minor: nil}
iex> Beacons.new(%{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"})
%Beacons{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: nil, minor: nil, relevant_text: nil}

iex> Beacons.new(%{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", relevant_text: "Welcome to our store!"})
%Beacons{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: nil, minor: nil, relevant_text: "Welcome to our store!"}

iex> Beacons.new(%{proximity_UUID: "invalid-uuid"})
** (ArgumentError) proximity_UUID must be a valid UUID string

iex> Beacons.new(%{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 65_536})
** (ArgumentError) major must be a 16-bit unsigned integer (0-65_535)

"""
@spec new(map()) :: %__MODULE__{}
Expand All @@ -58,7 +70,8 @@ defmodule ExPass.Structs.Beacons do
attrs
|> validate(:major, &Validators.validate_optional_16bit_unsigned_integer(&1, :major))
|> validate(:minor, &Validators.validate_optional_16bit_unsigned_integer(&1, :minor))
|> validate(:proximityUUID, &Validators.validate_uuid/1)
|> validate(:proximity_UUID, &Validators.validate_uuid/1)
|> validate(:relevant_text, &Validators.validate_optional_string(&1, :relevant_text))

struct!(__MODULE__, attrs)
end
Expand Down
13 changes: 8 additions & 5 deletions lib/utils/validators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -850,26 +850,29 @@ defmodule ExPass.Utils.Validators do
:ok

iex> validate_uuid("not-a-uuid")
{:error, "proximityUUID must be a valid UUID string"}
{:error, "proximity_UUID must be a valid UUID string"}

iex> validate_uuid(nil)
{:error, "proximityUUID is required"}
{:error, "proximity_UUID is required"}

iex> validate_uuid(123)
{:error, "proximity_UUID must be a valid UUID string"}

"""
@spec validate_uuid(String.t() | nil) :: :ok | {:error, String.t()}
def validate_uuid(nil), do: {:error, "proximityUUID is required"}
def validate_uuid(nil), do: {:error, "proximity_UUID is required"}

def validate_uuid(value) when is_binary(value) do
uuid_regex = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i

if Regex.match?(uuid_regex, value) do
:ok
else
{:error, "proximityUUID must be a valid UUID string"}
{:error, "proximity_UUID must be a valid UUID string"}
end
end

def validate_uuid(_), do: {:error, "proximityUUID must be a valid UUID string"}
def validate_uuid(_), do: {:error, "proximity_UUID must be a valid UUID string"}

defp validate_inclusion(value, valid_values, field_name) do
if value in valid_values do
Expand Down
107 changes: 84 additions & 23 deletions test/structs/beacons_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,63 @@ defmodule ExPass.Structs.BeaconsTest do
alias ExPass.Structs.Beacons

describe "new/0" do
test "raises an error for missing proximityUUID" do
assert_raise ArgumentError, "proximityUUID is required", fn ->
test "raises an error for missing proximity_UUID" do
assert_raise ArgumentError, "proximity_UUID is required", fn ->
Beacons.new()
end
end
end

describe "new/1 with minor field" do
test "creates a valid Beacons struct with minor field" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 50}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 50}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.minor == params.minor
assert beacon.major == nil
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("minor":50)
assert encoded =~ ~s("proximityuuid":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "creates a valid Beacons struct with minor field set to 0" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 0}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 0}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.minor == 0
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("minor":0)
assert encoded =~ ~s("proximityuuid":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "creates a valid Beacons struct with minor field set to 65535 (max value)" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 65_535}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 65_535}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.minor == 65_535
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("minor":65535)
assert encoded =~ ~s("proximityuuid":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "returns error for invalid minor (negative value)" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: -1}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: -1}

assert_raise ArgumentError, "minor must be a 16-bit unsigned integer (0-65_535)", fn ->
Beacons.new(params)
end
end

test "returns error for invalid minor (value too large)" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 65_536}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: 65_536}

assert_raise ArgumentError, "minor must be a 16-bit unsigned integer (0-65_535)", fn ->
Beacons.new(params)
end
end

test "returns error for invalid minor (non-integer value)" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: "50"}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", minor: "50"}

assert_raise ArgumentError, "minor must be a 16-bit unsigned integer (0-65_535)", fn ->
Beacons.new(params)
Expand All @@ -71,41 +71,102 @@ defmodule ExPass.Structs.BeaconsTest do

describe "new/1 with major field" do
test "creates a valid Beacons struct with major field" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 100}
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", major: 100}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.major == params.major
assert beacon.minor == nil
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("major":100)
assert encoded =~ ~s("proximityuuid":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end
end

describe "new/1 with proximityUUID field" do
test "creates a valid Beacons struct with proximityUUID field" do
params = %{proximityUUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"}
describe "new/1 with proximity_UUID field" do
test "creates a valid Beacons struct with proximity_UUID field" do
params = %{proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.proximityUUID == params.proximityUUID
assert beacon.proximity_UUID == params.proximity_UUID
assert beacon.major == nil
assert beacon.minor == nil
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("proximityuuid":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "returns error for invalid proximityUUID (not a UUID)" do
params = %{proximityUUID: "not-a-uuid"}
test "returns error for invalid proximity_UUID (not a UUID)" do
params = %{proximity_UUID: "not-a-uuid"}

assert_raise ArgumentError, "proximityUUID must be a valid UUID string", fn ->
assert_raise ArgumentError, "proximity_UUID must be a valid UUID string", fn ->
Beacons.new(params)
end
end

test "returns error for missing proximityUUID" do
test "returns error for missing proximity_UUID" do
params = %{major: 100, minor: 50}

assert_raise ArgumentError, "proximityUUID is required", fn ->
assert_raise ArgumentError, "proximity_UUID is required", fn ->
Beacons.new(params)
end
end

test "returns error for invalid proximity_UUID (non-string value)" do
params = %{proximity_UUID: 12_345}

assert_raise ArgumentError, "proximity_UUID must be a valid UUID string", fn ->
Beacons.new(params)
end
end

test "returns error for invalid proximity_UUID (nil value)" do
params = %{proximity_UUID: nil}

assert_raise ArgumentError, "proximity_UUID is required", fn ->
Beacons.new(params)
end
end
end

describe "new/1 with relevant_text field" do
test "creates a valid Beacons struct with relevant_text field" do
params = %{
proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
relevant_text: "Nearby store"
}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.relevant_text == params.relevant_text
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("relevantText":"Nearby store")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "creates a valid Beacons struct with all fields" do
params = %{
proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
major: 100,
minor: 50,
relevant_text: "Nearby store"
}

assert %Beacons{} = beacon = Beacons.new(params)
assert beacon.major == params.major
assert beacon.minor == params.minor
assert beacon.relevant_text == params.relevant_text
encoded = Jason.encode!(beacon)
assert encoded =~ ~s("major":100)
assert encoded =~ ~s("minor":50)
assert encoded =~ ~s("relevantText":"Nearby store")
assert encoded =~ ~s("proximityUUID":"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
end

test "raises an error for invalid relevant_text" do
params = %{
proximity_UUID: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
relevant_text: 123
}

assert_raise ArgumentError, "relevant_text must be a string if provided", fn ->
Beacons.new(params)
end
end
Expand Down