-module(jchat_prop_SUITE). -include_lib("common_test/include/ct.hrl"). -include_lib("proper/include/proper.hrl"). -include("../src/jchat.hrl"). %% CT callbacks -export([all/0, init_per_suite/1, end_per_suite/1]). %% Test cases -export([prop_id_validation/1, prop_conversation_crud/1, prop_message_crud/1, prop_json_encoding/1]). all() -> [prop_id_validation, prop_conversation_crud, prop_message_crud, prop_json_encoding]. init_per_suite(Config) -> application:ensure_all_started(jchat), timer:sleep(1000), Config. end_per_suite(_Config) -> application:stop(jchat), ok. %% Property tests prop_id_validation(_Config) -> ?assert(proper:quickcheck(?FORALL(Id, valid_id(), jchat_utils:validate_id(Id) =:= true))), ?assert(proper:quickcheck(?FORALL(Id, invalid_id(), jchat_utils:validate_id(Id) =:= false))). prop_conversation_crud(_Config) -> ?assert(proper:quickcheck(?FORALL({Id, Attrs}, {valid_id(), conversation_attrs()}, begin % Create {ok, Conv} = jchat_db:create_conversation(Id, Attrs), % Get {ok, Conv2} = jchat_db:get_conversation(Id), % Update Updates = #{title => <<"Updated Title">>}, {ok, Conv3} = jchat_db:update_conversation(Id, Updates), % Verify Conv#conversation.id =:= Conv2#conversation.id andalso Conv3#conversation.title =:= <<"Updated Title">> end))). prop_message_crud(_Config) -> ?assert(proper:quickcheck(?FORALL({ConvId, MsgId, Attrs}, {valid_id(), valid_id(), message_attrs()}, begin % Create conversation first ConvAttrs = #{title => <<"Test">>, participant_ids => [<<"user1">>]}, {ok, _} = jchat_db:create_conversation(ConvId, ConvAttrs), % Create message MsgAttrs = Attrs#{conversation_id => ConvId, sender_id => <<"user1">>}, {ok, Msg} = jchat_db:create_message(MsgId, MsgAttrs), % Get message {ok, Msg2} = jchat_db:get_message(MsgId), % Verify Msg#message.id =:= Msg2#message.id end))). prop_json_encoding(_Config) -> ?assert(proper:quickcheck(?FORALL(Data, json_data(), begin JSON = jchat_utils:json_encode(Data), {ok, Decoded} = jchat_utils:json_decode(JSON), normalize_json(Data) =:= normalize_json(Decoded) end))). %% Generators valid_id() -> ?LET(Chars, non_empty(list(oneof([ choose($A, $Z), choose($a, $z), choose($0, $9), return($-), return($_) ]))), list_to_binary(Chars)). invalid_id() -> oneof([ <<>>, % Empty ?LET(N, choose(256, 1000), list_to_binary(lists:duplicate(N, $a))), % Too long <<"invalid=chars">>, % Invalid characters <<"spaces not allowed">> % Spaces ]). conversation_attrs() -> ?LET({Title, Desc, Archived, Muted, Participants}, {binary(), oneof([binary(), null]), boolean(), boolean(), list(valid_id())}, #{title => Title, description => Desc, is_archived => Archived, is_muted => Muted, participant_ids => Participants}). message_attrs() -> ?LET({Body, BodyType}, {binary(), oneof([<<"text/plain">>, <<"text/html">>])}, #{body => Body, body_type => BodyType}). json_data() -> ?LAZY(oneof([ binary(), integer(), boolean(), null, list(json_data()), map(binary(), json_data()) ])). %% Helpers normalize_json(null) -> null; normalize_json(Data) when is_map(Data) -> maps:map(fun(_, V) -> normalize_json(V) end, Data); normalize_json(Data) when is_list(Data) -> [normalize_json(Item) || Item <- Data]; normalize_json(Data) -> Data.