Skip to content

Commit

Permalink
AppEngine: Implement property-based tests
Browse files Browse the repository at this point in the history
Implement property-based tests for `cast_value` functions.

Signed-off-by: Ismet Softic <ismet.softic@secomind.com>
  • Loading branch information
Hibe7 committed Jul 18, 2024
1 parent c737fc5 commit 6168b87
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [1.2.0] - 2024-07-02
### Added
- [astarte_appengine_api] Implement property-based tests for `cast_value` functions.

### Fixed
- Forward port changes from release-1.1 (connection failure when delivering
triggers is handled as an error).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

defmodule Astarte.AppEngine.API.InterfaceValueTest do
use ExUnit.Case
use ExUnitProperties
alias Astarte.AppEngine.API.Device.InterfaceValue
alias Astarte.AppEngine.API.InterfaceValueTestGenerator

test "cast datetime values with valid data" do
assert InterfaceValue.cast_value(:datetime, "2024-06-20T14:02:05.371Z") ==
Expand Down Expand Up @@ -172,4 +174,201 @@ defmodule Astarte.AppEngine.API.InterfaceValueTest do
assert InterfaceValue.cast_value(expected_types, object) ==
{:error, :unexpected_value_type, [expected: :longinteger]}
end

property "casting valid datetime values" do
check all datetime <- InterfaceValueTestGenerator.gen_datetime() do
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)

assert InterfaceValue.cast_value(:datetime, datetime) ==
{:ok, parsed_datetime}
end
end

property "casting valid datetime array values" do
check all datetime_array <- InterfaceValueTestGenerator.gen_datetime_array() do
parsed_datetimes =
Enum.map(datetime_array, fn datetime ->
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
parsed_datetime
end)

assert InterfaceValue.cast_value(:datetimearray, datetime_array) ==
{:ok, parsed_datetimes}
end
end

property "casting valid binary blob values" do
check all blob <- StreamData.binary() do
base64_blob = Base.encode64(blob)

assert InterfaceValue.cast_value(:binaryblob, base64_blob) ==
{:ok, blob}
end

check all binary_blob_array <- InterfaceValueTestGenerator.gen_binaryblob_array() do
parsed_blobs = Enum.map(binary_blob_array, &Base.decode64!/1)

assert InterfaceValue.cast_value(:binaryblobarray, binary_blob_array) ==
{:ok, parsed_blobs}
end
end

property "casting valid double values" do
check all number <- StreamData.float() do
assert InterfaceValue.cast_value(:double, number) ==
{:ok, number}
end

check all double_array <- InterfaceValueTestGenerator.gen_double_array() do
assert InterfaceValue.cast_value(:doublearray, double_array) ==
{:ok, double_array}
end
end

property "casting valid long integer values" do
check all integer <- StreamData.integer() do
assert InterfaceValue.cast_value(:longinteger, integer) ==
{:ok, integer}

string_integer = Integer.to_string(integer)

assert InterfaceValue.cast_value(:longinteger, string_integer) ==
{:ok, integer}
end
end

property "casting valid long integer array values" do
check all long_integer_array <- InterfaceValueTestGenerator.gen_longinteger_array() do
parsed_integers =
Enum.map(long_integer_array, fn
value when is_integer(value) -> value
value when is_binary(value) -> String.to_integer(value)
end)

assert InterfaceValue.cast_value(:longintegerarray, long_integer_array) ==
{:ok, parsed_integers}
end
end

property "casting valid other types" do
check all string <- StreamData.string(:printable) do
assert InterfaceValue.cast_value(:string, string) ==
{:ok, string}
end

check all string_array <- InterfaceValueTestGenerator.gen_string_array() do
assert InterfaceValue.cast_value(:stringarray, string_array) ==
{:ok, string_array}
end

check all integer <- StreamData.integer() do
assert InterfaceValue.cast_value(:integer, integer) ==
{:ok, integer}
end

check all integer_array <- InterfaceValueTestGenerator.gen_integer_array() do
assert InterfaceValue.cast_value(:integerarray, integer_array) ==
{:ok, integer_array}
end

check all boolean <- StreamData.boolean() do
assert InterfaceValue.cast_value(:boolean, boolean) ==
{:ok, boolean}
end

check all boolean_array <- InterfaceValueTestGenerator.gen_boolean_array() do
assert InterfaceValue.cast_value(:booleanarray, boolean_array) ==
{:ok, boolean_array}
end
end

property "casting invalid datetime values" do
check all invalid_datetime <- InterfaceValueTestGenerator.gen_invalid_datetime() do
assert InterfaceValue.cast_value(:datetime, invalid_datetime) ==
{:error, :unexpected_value_type, [expected: :datetime]}
end

check all invalid_datetime_array <- InterfaceValueTestGenerator.gen_invalid_datetime_array() do
assert InterfaceValue.cast_value(:datetimearray, invalid_datetime_array) ==
{:error, :unexpected_value_type, [expected: :datetimearray]}
end
end

property "casting invalid binary blob values" do
check all invalid_blob <- InterfaceValueTestGenerator.gen_invalid_binaryblob() do
assert InterfaceValue.cast_value(:binaryblob, invalid_blob) ==
{:error, :unexpected_value_type, [expected: :binaryblob]}
end

check all invalid_binary_blob_array <-
InterfaceValueTestGenerator.gen_invalid_binaryblob_array() do
assert InterfaceValue.cast_value(:binaryblobarray, invalid_binary_blob_array) ==
{:error, :unexpected_value_type, [expected: :binaryblobarray]}
end
end

property "casting invalid double values" do
check all string <- StreamData.string(:printable) do
assert InterfaceValue.cast_value(:double, string) ==
{:error, :unexpected_value_type, [expected: :double]}
end

check all invalid_double_array <- InterfaceValueTestGenerator.gen_invalid_double_array() do
assert InterfaceValue.cast_value(:doublearray, invalid_double_array) ==
{:error, :unexpected_value_type, [expected: :doublearray]}
end
end

property "casting invalid long integer values" do
check all string <- StreamData.string(:printable, min_length: 1, max_length: 10) do
assert InterfaceValue.cast_value(:longinteger, string) ==
{:error, :unexpected_value_type, [expected: :longinteger]}
end

check all invalid_longinteger_array <-
InterfaceValueTestGenerator.gen_invalid_longinteger_array() do
assert InterfaceValue.cast_value(:longintegerarray, invalid_longinteger_array) ==
{:error, :unexpected_value_type, [expected: :longintegerarray]}
end
end

property "casting valid object values" do
expected_types = %{
"a" => :integer,
"b" => :string
}

check all object <- InterfaceValueTestGenerator.gen_object(expected_types) do
assert InterfaceValue.cast_value(expected_types, object) ==
{:ok, object}
end
end

property "returns error when object key is not in expected types" do
expected_types = %{
"a" => :integer,
"b" => :string
}

check all object <-
InterfaceValueTestGenerator.gen_object(Map.put(expected_types, "c", :string)) do
assert InterfaceValue.cast_value(expected_types, object) == {:error, :unexpected_object_key}
end
end

property "returns error when value type does not match expected type" do
expected_types = %{
"a" => :longinteger,
"b" => :string
}

check all object <-
StreamData.fixed_map(%{
"a" => StreamData.string(:printable),
"b" => StreamData.string(:printable)
}) do
assert InterfaceValue.cast_value(expected_types, object) ==
{:error, :unexpected_value_type, [expected: :longinteger]}
end
end
end
128 changes: 128 additions & 0 deletions apps/astarte_appengine_api/test/support/generators/interface-value.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#
# This file is part of Astarte.
#
# Copyright 2024 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

defmodule Astarte.AppEngine.API.InterfaceValueTestGenerator do
use ExUnitProperties

def gen_datetime() do
StreamData.fixed_map(%{
year: StreamData.integer(2020..2030),
month: StreamData.integer(1..12),
day: StreamData.integer(1..28),
hour: StreamData.integer(0..23),
minute: StreamData.integer(0..59),
second: StreamData.integer(0..59),
millisecond: StreamData.integer(0..999)
})
|> StreamData.map(fn %{
year: y,
month: mo,
day: d,
hour: h,
minute: mi,
second: s,
millisecond: ms
} ->
{:ok, date} = Date.new(y, mo, d)
{:ok, time} = Time.new(h, mi, s, {ms, 3})
{:ok, datetime} = DateTime.new(date, time, "Etc/UTC")
DateTime.to_iso8601(datetime)
end)
end

def gen_datetime_array() do
StreamData.list_of(gen_datetime(), min_length: 1, max_length: 10)
end

def gen_invalid_datetime() do
StreamData.string(:printable, min_length: 1, max_length: 30)
|> StreamData.filter(&(!String.match?(&1, ~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)))
end

def gen_invalid_datetime_array() do
StreamData.list_of(gen_invalid_datetime(), min_length: 1, max_length: 10)
end

def gen_binaryblob() do
StreamData.binary(min_length: 1, max_length: 10)
|> StreamData.map(&Base.encode64/1)
end

def gen_binaryblob_array() do
StreamData.list_of(gen_binaryblob(), min_length: 1, max_length: 10)
end

def gen_invalid_binaryblob() do
StreamData.string(:printable, min_length: 1, max_length: 10)
|> StreamData.filter(&(!String.match?(&1, ~r/^[A-Za-z0-9\/\+\=]{0,2}$/)))
end

def gen_invalid_binaryblob_array() do
StreamData.list_of(gen_invalid_binaryblob(), min_length: 1, max_length: 10)
end

def gen_double_array() do
StreamData.list_of(StreamData.float(), min_length: 1, max_length: 10)
end

def gen_invalid_double_array do
StreamData.list_of(StreamData.string(:printable), min_length: 1)
end

def gen_longinteger_array() do
StreamData.one_of([
StreamData.list_of(StreamData.integer(), min_length: 1, max_length: 10),
StreamData.list_of(StreamData.map(StreamData.integer(), &Integer.to_string/1),
min_length: 1,
max_length: 10
)
])
end

def gen_invalid_longinteger() do
StreamData.string(:printable, min_length: 1, max_length: 10)
end

def gen_invalid_longinteger_array() do
StreamData.list_of(gen_invalid_longinteger(), min_length: 1, max_length: 10)
end

def gen_string_array() do
StreamData.list_of(StreamData.string(:printable), min_length: 1, max_length: 10)
end

def gen_integer_array() do
StreamData.list_of(StreamData.integer(), min_length: 1, max_length: 10)
end

def gen_boolean_array() do
StreamData.list_of(StreamData.boolean(), min_length: 1, max_length: 10)
end

def gen_object(expected_types) do
StreamData.fixed_map(
Enum.into(expected_types, %{}, fn {key, type} ->
{key, gen_value(type)}
end)
)
end

defp gen_value(:integer), do: StreamData.integer()
defp gen_value(:string), do: StreamData.string(:printable)
defp gen_value(:longinteger), do: StreamData.integer()
end

0 comments on commit 6168b87

Please sign in to comment.