diff --git a/config/ct.config b/config/ct.config index 8f05353..4f2695b 100644 --- a/config/ct.config +++ b/config/ct.config @@ -72,6 +72,7 @@ grpc_opts => #{ service_protos => [iot_config_pb], services => #{ + 'helium.iot_config.org' => hpr_test_ics_org_service, 'helium.iot_config.gateway' => hpr_test_ics_gateway_service, 'helium.iot_config.route' => hpr_test_ics_route_service } diff --git a/src/cli/hpr_cli_config.erl b/src/cli/hpr_cli_config.erl index 55c1618..6b1708a 100644 --- a/src/cli/hpr_cli_config.erl +++ b/src/cli/hpr_cli_config.erl @@ -6,8 +6,23 @@ -behavior(clique_handler). +-include("hpr.hrl"). + -export([register_cli/0]). +-export([ + get_api_routes/0, + get_api_routes_for_oui/1 +]). + +-ifdef(TEST). + +-export([ + config_route_sync/3 +]). + +-endif. + register_cli() -> register_all_usage(), register_all_cmds(). @@ -42,9 +57,11 @@ config_usage() -> " [--display_skfs] default: false (SKFs not included)\n", "config route refresh_all - Refresh all routes\n", " [--minimum] default: 1 (need a minimum of 1 SKFs ro be updated)\n", - "config route refresh - Refresh route\n", + "config route refresh - Refresh route's EUIs, SKFs, DevAddrRanges\n", "config route activate - Activate route\n", "config route deactivate - Deactivate route\n", + "config route remove - Delete all remnants of a route\n", + "config route sync [--oui=] - Fetch all Routes from Config Service, creating new, removing old\n" "config skf - List all Session Key Filters for Devaddr or Session Key\n", "config eui --app --dev - List all Routes with EUI pair\n" "\n\n", @@ -104,6 +121,18 @@ config_cmd() -> [], fun config_route_deactivate/3 ], + [ + ["config", "route", "remove", '*'], + [], + [], + fun config_route_remove/3 + ], + [ + ["config", "route", "sync"], + [], + [{oui, [{shortname, "o"}, {longname, "oui"}]}], + fun config_route_sync/3 + ], [["config", "skf", '*'], [], [], fun config_skf/3], [ ["config", "eui"], @@ -398,6 +427,45 @@ config_route_deactivate(["config", "route", "deactivate", RouteID], [], _Flags) config_route_deactivate(_, _, _) -> usage. +config_route_remove(["config", "route", "remove", RouteID], [], []) -> + case hpr_route_storage:lookup(RouteID) of + {ok, RouteETS} -> + Route = hpr_route_ets:route(RouteETS), + hpr_route_storage:delete(Route), + c_text("Deleted Route: ~p", [RouteID]); + {error, not_found} -> + c_text("Could not find ~p", [RouteID]) + end; +config_route_remove(_, _, _) -> + usage. + +config_route_sync(["config", "route", "sync"], [], Flags) -> + Updates = + case maps:from_list(Flags) of + #{oui := OUI0} -> + OUI = erlang:list_to_integer(OUI0, 10), + APIRoutes = get_api_routes_for_oui(OUI), + ExistingRoutes = hpr_route_storage:oui_routes(OUI), + sync_routes(APIRoutes, ExistingRoutes); + #{} -> + APIRoutes = get_api_routes(), + ExistingRoutes = hpr_route_storage:all_routes(), + sync_routes(APIRoutes, ExistingRoutes) + end, + + FormatRoute = fun(Route) -> + io_lib:format(" - ~s OUI=~p~n", [hpr_route:id(Route), hpr_route:oui(Route)]) + end, + Added = lists:map(FormatRoute, maps:get(added, Updates, [])), + Removed = lists:map(FormatRoute, maps:get(removed, Updates, [])), + + c_list( + [io_lib:format("=== Added (~p) ===~n", [length(Added)])] ++ Added ++ + [io_lib:format("=== Removed (~p) ===~n", [length(Removed)])] ++ Removed + ); +config_route_sync(_, _, _) -> + usage. + config_skf(["config", "skf", DevAddrOrSKF], [], []) -> SKFS = case hpr_utils:hex_to_bin(erlang:list_to_binary(DevAddrOrSKF)) of @@ -728,3 +796,81 @@ format_skf({{SKF, DevAddr}, MaxCopies}) -> hpr_utils:bin_to_hex_string(SKF), MaxCopies ]). + +-spec get_api_routes() -> list(hpr_route:route()). +get_api_routes() -> + {ok, OrgList, _Meta} = helium_iot_config_org_client:list( + hpr_org_list_req:new(), + #{channel => ?IOT_CONFIG_CHANNEL} + ), + + lists:flatmap( + fun(OUI) -> get_api_routes_for_oui(OUI) end, + hpr_org_list_res:org_ouis(OrgList) + ). + +-spec get_api_routes_for_oui(OUI :: non_neg_integer()) -> list(hpr_route:route()). +get_api_routes_for_oui(OUI) -> + PubKeyBin = hpr_utils:pubkey_bin(), + SigFun = hpr_utils:sig_fun(), + + ListReq = hpr_route_list_req:new(PubKeyBin, OUI), + SignedReq = hpr_route_list_req:sign(ListReq, SigFun), + {ok, RouteListRes, _Meta} = helium_iot_config_route_client:list( + SignedReq, + #{channel => ?IOT_CONFIG_CHANNEL} + ), + hpr_route_list_res:routes(RouteListRes). + +-spec sync_routes( + APIRoutes :: list(hpr_route:route()), + ExistingRoutes :: list(hpr_route:route()) +) -> #{added => list(hpr_route:route()), removed => list(hpr_route:route())}. +sync_routes(APIRoutes, ExistingRoutes) -> + sync_routes(APIRoutes, ExistingRoutes, #{added => [], removed => []}). + +-spec sync_routes( + APIRoutes :: list(hpr_route:route()), + ExistingRoutes :: list(hpr_route:route()), + Updates :: map() +) -> #{added => list(hpr_route:route()), removed => list(hpr_route:route())}. +sync_routes([], [], Updates) -> + Updates; +sync_routes([], [Route | LeftoverRoutes], #{removed := RemovedRoutes} = Updates) -> + RouteID = hpr_route:id(Route), + lager:info([{route_id, RouteID}], "removing leftover route: ~p"), + ok = hpr_route_storage:delete(Route), + sync_routes( + [], + LeftoverRoutes, + Updates#{removed => [Route | RemovedRoutes]} + ); +sync_routes([Route | Routes], ExistingRoutes, #{added := AddedRoutes} = Updates) -> + RouteID = hpr_route:id(Route), + case hpr_route_storage:lookup(RouteID) of + {ok, _Route} -> + lager:info([{route_id, RouteID}], "doing nothing, route already exists"), + sync_routes( + Routes, + remove_route(Route, ExistingRoutes), + Updates + ); + {error, not_found} -> + lager:info([{route_id, RouteID}], "syncing new route"), + ok = hpr_route_storage:insert(Route), + hpr_route_stream_worker:refresh_route(hpr_route:id(Route)), + + sync_routes( + Routes, + remove_route(Route, ExistingRoutes), + Updates#{added => [Route | AddedRoutes]} + ) + end. + +-spec remove_route( + Target :: hpr_route:route(), + Coll :: list(hpr_route:route()) +) -> list(hpr_route:route()). +remove_route(Target, RouteList) -> + ID = hpr_route:id(Target), + lists:filter(fun(R) -> hpr_route:id(R) =/= ID end, RouteList). diff --git a/src/grpc/iot_config/hpr_org.erl b/src/grpc/iot_config/hpr_org.erl new file mode 100644 index 0000000..abe1b07 --- /dev/null +++ b/src/grpc/iot_config/hpr_org.erl @@ -0,0 +1,56 @@ +-module(hpr_org). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + oui/1, + owner/1, + payer/1, + delegate_keys/1, + locked/1 +]). + +-type org() :: #iot_config_org_v1_pb{}. + +-ifdef(TEST). + +-export([test_new/1]). + +-endif. + +-spec oui(Org :: org()) -> non_neg_integer(). +oui(Org) -> + Org#iot_config_org_v1_pb.oui. + +-spec owner(Org :: org()) -> binary(). +owner(Org) -> + Org#iot_config_org_v1_pb.owner. + +-spec payer(Org :: org()) -> binary(). +payer(Org) -> + Org#iot_config_org_v1_pb.payer. + +-spec delegate_keys(Org :: org()) -> list(binary()). +delegate_keys(Org) -> + Org#iot_config_org_v1_pb.delegate_keys. + +-spec locked(Org :: org()) -> boolean(). +locked(Org) -> + Org#iot_config_org_v1_pb.locked. + +%% ------------------------------------------------------------------ +%% Tests Functions +%% ------------------------------------------------------------------ +-ifdef(TEST). + +-spec test_new(RouteMap :: map()) -> org(). +test_new(RouteMap) -> + #iot_config_org_v1_pb{ + oui = maps:get(oui, RouteMap), + owner = maps:get(owner, RouteMap, <<"owner-test-value">>), + payer = maps:get(payer, RouteMap, <<"payer-test-value">>), + delegate_keys = maps:get(delegate_keys, RouteMap, []), + locked = maps:get(locked, RouteMap, false) + }. + +-endif. diff --git a/src/grpc/iot_config/hpr_org_list_req.erl b/src/grpc/iot_config/hpr_org_list_req.erl new file mode 100644 index 0000000..e75fe8e --- /dev/null +++ b/src/grpc/iot_config/hpr_org_list_req.erl @@ -0,0 +1,11 @@ +-module(hpr_org_list_req). + +-include("../autogen/iot_config_pb.hrl"). + +-export([new/0]). + +-type req() :: #iot_config_org_list_req_v1_pb{}. + +-spec new() -> req(). +new() -> + #iot_config_org_list_req_v1_pb{}. diff --git a/src/grpc/iot_config/hpr_org_list_res.erl b/src/grpc/iot_config/hpr_org_list_res.erl new file mode 100644 index 0000000..e5a7a42 --- /dev/null +++ b/src/grpc/iot_config/hpr_org_list_res.erl @@ -0,0 +1,36 @@ +-module(hpr_org_list_res). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + orgs/1, + timestamp/1, + signer/1, + signature/1 +]). + +-export([ + org_ouis/1 +]). + +-type res() :: #iot_config_org_list_res_v1_pb{}. + +-spec orgs(Res :: res()) -> list(hpr_org:org()). +orgs(Res) -> + Res#iot_config_org_list_res_v1_pb.orgs. + +-spec timestamp(Res :: res()) -> non_neg_integer(). +timestamp(Res) -> + Res#iot_config_org_list_res_v1_pb.timestamp. + +-spec signer(Res :: res()) -> binary(). +signer(Res) -> + Res#iot_config_org_list_res_v1_pb.signer. + +-spec signature(Res :: res()) -> binary(). +signature(Res) -> + Res#iot_config_org_list_res_v1_pb.signature. + +-spec org_ouis(Res :: res()) -> list(non_neg_integer()). +org_ouis(Res) -> + [hpr_org:oui(Org) || Org <- ?MODULE:orgs(Res)]. diff --git a/src/grpc/iot_config/hpr_route_list_req.erl b/src/grpc/iot_config/hpr_route_list_req.erl new file mode 100644 index 0000000..d52339a --- /dev/null +++ b/src/grpc/iot_config/hpr_route_list_req.erl @@ -0,0 +1,58 @@ +-module(hpr_route_list_req). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + new/2, + timestamp/1, + signer/1, + signature/1 +]). + +-export([ + sign/2, + verify/1 +]). + +-type req() :: #iot_config_route_list_req_v1_pb{}. + +-spec new(Signer :: libp2p_crypto:pubkey_bin(), Oui :: non_neg_integer()) -> req(). +new(Signer, Oui) -> + #iot_config_route_list_req_v1_pb{ + oui = Oui, + timestamp = erlang:system_time(millisecond), + signer = Signer + }. + +-spec timestamp(Req :: req()) -> non_neg_integer(). +timestamp(Req) -> + Req#iot_config_route_list_req_v1_pb.timestamp. + +-spec signer(Req:: req()) -> binary(). +signer(Req) -> + Req#iot_config_route_list_req_v1_pb.signer. + +-spec signature(Req:: req()) -> binary(). +signature(Req) -> + Req#iot_config_route_list_req_v1_pb.signature. + +-spec sign(RouteListReq :: req(), SigFun :: fun()) -> req(). +sign(RouteListReq, SigFun) -> + EncodedRouteListReq = iot_config_pb:encode_msg( + RouteListReq, iot_config_route_list_req_v1_pb + ), + RouteListReq#iot_config_route_list_req_v1_pb{signature = SigFun(EncodedRouteListReq)}. + +-spec verify(RouteListReq :: req()) -> boolean(). +verify(RouteListReq) -> + EncodedRouteListReq = iot_config_pb:encode_msg( + RouteListReq#iot_config_route_list_req_v1_pb{ + signature = <<>> + }, + iot_config_route_list_req_v1_pb + ), + libp2p_crypto:verify( + EncodedRouteListReq, + ?MODULE:signature(RouteListReq), + libp2p_crypto:bin_to_pubkey(?MODULE:signer(RouteListReq)) + ). diff --git a/src/grpc/iot_config/hpr_route_list_res.erl b/src/grpc/iot_config/hpr_route_list_res.erl new file mode 100644 index 0000000..96ab0b6 --- /dev/null +++ b/src/grpc/iot_config/hpr_route_list_res.erl @@ -0,0 +1,13 @@ +-module(hpr_route_list_res). + +-include("../autogen/iot_config_pb.hrl"). + +-export([ + routes/1 +]). + +-type res() :: #iot_config_route_list_res_v1_pb{}. + +-spec routes(Res :: res()) -> list(hpr_route:route()). +routes(Res) -> + Res#iot_config_route_list_res_v1_pb.routes. diff --git a/test/hpr_cli_config_SUITE.erl b/test/hpr_cli_config_SUITE.erl new file mode 100644 index 0000000..3f78341 --- /dev/null +++ b/test/hpr_cli_config_SUITE.erl @@ -0,0 +1,342 @@ +-module(hpr_cli_config_SUITE). + +-include_lib("eunit/include/eunit.hrl"). + +-export([ + all/0, + init_per_testcase/2, + end_per_testcase/2 +]). + +-export([ + sync_new_route_test/1, + sync_remove_route_test/1, + sync_new_remove_route_test/1, + sync_oui_only_test/1 +]). + +%%-------------------------------------------------------------------- +%% COMMON TEST CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% Running tests for this suite +%% @end +%%-------------------------------------------------------------------- +all() -> + [ + sync_new_route_test, + sync_remove_route_test, + sync_new_remove_route_test, + sync_oui_only_test + ]. + +%%-------------------------------------------------------------------- +%% TEST CASE SETUP +%%-------------------------------------------------------------------- +init_per_testcase(TestCase, Config) -> + test_utils:init_per_testcase(TestCase, Config). + +%%-------------------------------------------------------------------- +%% TEST CASE TEARDOWN +%%-------------------------------------------------------------------- +end_per_testcase(TestCase, Config) -> + meck:unload(), + test_utils:end_per_testcase(TestCase, Config), + ok. + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +sync_oui_only_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + #{ + route_id := Route1ID, + route := Route1, + eui_pair := EUIPair1, + devaddr_range := DevAddrRange1, + skf := SessionKeyFilter1 + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure a different route is setup to come back, removing the original, + %% and adding the new. + #{ + route_id := Route2ID, + route := Route2, + eui_pair := EUIPair2, + devaddr_range := DevAddrRange2, + skf := SessionKeyFilter2 + } = test_data("7d502f32-4d58-4746-965e-002", 2), + application:set_env(hpr, test_route_list, [Route2]), + application:set_env(hpr, test_route_get_euis, [EUIPair2]), + application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange2]), + application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter2]), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], [ + {oui, "2"} + ]), + %% ct:print("~s", [_Output]), + + % Route 1 was not removed + ok = check_config_counts(Route1ID, 2, 2, 2, 1), + ok = check_config_counts(Route2ID, 2, 2, 2, 1), + + ok. + +sync_new_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + %% Route does not exist locally. It will be added when + %% hpr_route_stream_worker:sync_routes() is called. + #{ + route_id := RouteID, + route := Route, + eui_pair := EUIPair, + devaddr := DevAddr, + devaddr_range := DevAddrRange, + session_key := SessionKey, + skf := SessionKeyFilter + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = application:set_env(hpr, test_route_list, [Route]), + ok = application:set_env(hpr, test_route_get_euis, [EUIPair]), + ok = application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange]), + ok = application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter]), + + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + {ok, RouteETS} = hpr_route_storage:lookup(RouteID), + + ?assertEqual([RouteETS], hpr_devaddr_range_storage:lookup(16#00000005)), + ?assertEqual([RouteETS], hpr_eui_pair_storage:lookup(1, 12)), + SK1 = hpr_utils:hex_to_bin(SessionKey), + SKFEts = hpr_route_ets:skf_ets(RouteETS), + ?assertEqual([{SK1, 1}], hpr_skf_storage:lookup(SKFEts, DevAddr)), + + ok. + +sync_new_remove_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + #{ + route_id := Route1ID, + route := Route1, + eui_pair := EUIPair1, + devaddr := DevAddr1, + devaddr_range := DevAddrRange1, + skf := SessionKeyFilter1 + } = test_data("7d502f32-4d58-4746-965e-001"), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure a different route is setup to come back, removing the original, + %% and adding the new. + #{ + route_id := Route2ID, + route := Route2, + eui_pair := EUIPair2, + devaddr := DevAddr2, + devaddr_range := DevAddrRange2, + skf := SessionKeyFilter2 + } = test_data("7d502f32-4d58-4746-965e-002", 2), + application:set_env(hpr, test_route_list, [Route2]), + application:set_env(hpr, test_route_get_euis, [EUIPair2]), + application:set_env(hpr, test_route_get_devaddr_ranges, [DevAddrRange2]), + application:set_env(hpr, test_route_list_skfs, [SessionKeyFilter2]), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + ok = check_config_counts(Route2ID, 1, 1, 1, 1), + + ?assertEqual({error, not_found}, hpr_route_storage:lookup(Route1ID)), + %% New route uses same values as the old one, except for ID. + {ok, RouteETS2} = hpr_route_storage:lookup(Route2ID), + ?assertEqual([RouteETS2], hpr_devaddr_range_storage:lookup(DevAddr1)), + ?assertEqual([RouteETS2], hpr_eui_pair_storage:lookup(1, 12)), + + SK2_0 = hpr_skf:session_key(SessionKeyFilter2), + SK2 = hpr_utils:hex_to_bin(SK2_0), + SKFEts = hpr_route_ets:skf_ets(RouteETS2), + ?assertEqual([{SK2, 1}], hpr_skf_storage:lookup(SKFEts, DevAddr2)), + + ok. + +sync_remove_route_test(_Config) -> + application:set_env(hpr, test_org_service_orgs, [ + hpr_org:test_new(#{oui => 1}), + hpr_org:test_new(#{oui => 2}) + ]), + + Route1ID = "7d502f32-4d58-4746-965e-001", + Route1 = hpr_route:test_new(#{ + id => Route1ID, + net_id => 0, + oui => 1, + server => #{ + host => "localhost", + port => 8080, + protocol => {packet_router, #{}} + }, + max_copies => 10 + }), + EUIPair1 = hpr_eui_pair:test_new(#{route_id => Route1ID, app_eui => 1, dev_eui => 0}), + DevAddrRange1 = hpr_devaddr_range:test_new(#{ + route_id => Route1ID, start_addr => 16#00000001, end_addr => 16#0000000A + }), + DevAddr1 = 16#00000001, + SessionKey1 = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), + SessionKeyFilter1 = hpr_skf:new(#{ + route_id => Route1ID, devaddr => DevAddr1, session_key => SessionKey1, max_copies => 1 + }), + + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {route, Route1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {eui_pair, EUIPair1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {devaddr_range, DevAddrRange1}}) + ), + ok = hpr_test_ics_route_service:stream_resp( + hpr_route_stream_res:test_new(#{action => add, data => {skf, SessionKeyFilter1}}) + ), + ok = check_config_counts(Route1ID, 1, 1, 1, 1), + + %% Ensure nothing is meant to come back from the config service when syncing. + application:set_env(hpr, test_route_list, []), + application:set_env(hpr, test_route_get_euis, []), + application:set_env(hpr, test_route_get_devaddr_ranges, []), + application:set_env(hpr, test_route_list_skfs, []), + + %% Syncing Routes removes the route that no longer exists + [{list, _Output}] = hpr_cli_config:config_route_sync(["config", "route", "sync"], [], []), + %% ct:print("~s", [_Output]), + ?assertEqual({error, not_found}, hpr_route_storage:lookup(Route1ID)), + ?assertEqual([], hpr_devaddr_range_storage:lookup(DevAddr1)), + ?assertEqual([], hpr_eui_pair_storage:lookup(1, 12)), + ?assertEqual([], hpr_skf_storage:lookup_route(Route1ID)), + + ok. + +%% =================================================================== +%% Helpers +%% =================================================================== + +test_data(RouteID) -> + test_data(RouteID, 1). + +test_data(RouteID, OUI) -> + Route1 = hpr_route:test_new(#{ + id => RouteID, + net_id => 0, + oui => OUI, + server => #{ + host => "localhost", + port => 8080, + protocol => {packet_router, #{}} + }, + max_copies => 10 + }), + EUIPair1 = hpr_eui_pair:test_new(#{ + route_id => RouteID, app_eui => 1, dev_eui => 0 + }), + DevAddrRange1 = hpr_devaddr_range:test_new(#{ + route_id => RouteID, start_addr => 16#00000001, end_addr => 16#0000000A + }), + DevAddr1 = 16#00000001, + SessionKey1 = hpr_utils:bin_to_hex_string(crypto:strong_rand_bytes(16)), + SessionKeyFilter1 = hpr_skf:new(#{ + route_id => RouteID, + devaddr => DevAddr1, + session_key => SessionKey1, + max_copies => 1 + }), + #{ + route_id => RouteID, + route => Route1, + eui_pair => EUIPair1, + devaddr => DevAddr1, + devaddr_range => DevAddrRange1, + session_key => SessionKey1, + skf => SessionKeyFilter1 + }. + +check_config_counts( + RouteID, + ExpectedRouteCount, + ExpectedEUIPairCount, + ExpectedDevaddrRangeCount, + %% NOTE: SKF are separated by Route, provide amount expected for RouteID + ExpectedSKFCount +) -> + ok = test_utils:wait_until( + fun() -> + case hpr_route_storage:lookup(RouteID) of + {ok, RouteETS} -> + RouteCount = ets:info(hpr_routes_ets, size), + EUIPairCount = ets:info(hpr_route_eui_pairs_ets, size), + DevaddrRangeCount = ets:info(hpr_route_devaddr_ranges_ets, size), + SKFCount = ets:info(hpr_route_ets:skf_ets(RouteETS), size), + + { + ExpectedRouteCount =:= RouteCount andalso + ExpectedEUIPairCount =:= EUIPairCount andalso + ExpectedDevaddrRangeCount =:= DevaddrRangeCount andalso + ExpectedSKFCount =:= SKFCount, + [ + {route_id, RouteID}, + {route, ExpectedRouteCount, RouteCount}, + {eui_pair, ExpectedEUIPairCount, EUIPairCount}, + {devaddr_range, ExpectedDevaddrRangeCount, DevaddrRangeCount}, + {skf, ExpectedSKFCount, SKFCount}, + {skf_items, ets:tab2list(hpr_route_ets:skf_ets(RouteETS))} + ] + }; + _ -> + {false, {route_not_found, RouteID}, + {all_routes, [hpr_route_storage:all_routes()]}} + end + end + ). diff --git a/test/hpr_route_stream_worker_SUITE.erl b/test/hpr_route_stream_worker_SUITE.erl index d2859d4..8e24827 100644 --- a/test/hpr_route_stream_worker_SUITE.erl +++ b/test/hpr_route_stream_worker_SUITE.erl @@ -725,7 +725,9 @@ test_data(RouteID) -> route_id => RouteID, route => Route1, eui_pair => EUIPair1, + devaddr => DevAddr1, devaddr_range => DevAddrRange1, + session_key => SessionKey1, skf => SessionKeyFilter1 }. diff --git a/test/hpr_test_ics_org_service.erl b/test/hpr_test_ics_org_service.erl new file mode 100644 index 0000000..aa6cbbb --- /dev/null +++ b/test/hpr_test_ics_org_service.erl @@ -0,0 +1,55 @@ +-module(hpr_test_ics_org_service). + +-behaviour(helium_iot_config_org_bhvr). + +-include("../src/grpc/autogen/iot_config_pb.hrl"). + +-export([ + init/2, + handle_info/2 +]). + +-export([ + list/2, + get/2, + create_helium/2, + create_roamer/2, + update/2, + disable/2, + enable/2 +]). + +-spec init(atom(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). +init(_RPC, StreamState) -> + StreamState. + +-spec handle_info(Msg :: any(), StreamState :: grpcbox_stream:t()) -> grpcbox_stream:t(). +handle_info(_Msg, StreamState) -> + StreamState. + +list(Ctx, _Msg) -> + OrgList = #iot_config_org_list_res_v1_pb{ + orgs = application:get_env(hpr, test_org_service_orgs, []), + timestamp = erlang:system_time(millisecond), + signer = <<>>, + signature = <<>> + }, + {ok, OrgList, Ctx}. + +get(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +create_helium(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +create_roamer(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +update(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +disable(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. + +enable(_Ctx, _Msg) -> + {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. diff --git a/test/hpr_test_ics_route_service.erl b/test/hpr_test_ics_route_service.erl index b9cbe6f..51e6f74 100644 --- a/test/hpr_test_ics_route_service.erl +++ b/test/hpr_test_ics_route_service.erl @@ -67,8 +67,35 @@ handle_info({stream_resp, RouteStreamResp}, StreamState) -> handle_info(_Msg, StreamState) -> StreamState. -list(_Ctx, _Msg) -> - {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}. +list(Ctx, RouteListReq) -> + Encoded = iot_config_pb:encode_msg(RouteListReq#iot_config_route_list_req_v1_pb{ + signature = <<>> + }), + try + libp2p_crypto:verify( + Encoded, + RouteListReq#iot_config_route_list_req_v1_pb.signature, + libp2p_crypto:bin_to_pubkey(RouteListReq#iot_config_route_list_req_v1_pb.signer) + ) + of + false -> + {grpc_error, {grpcbox_stream:code_to_status(7), <<"PERMISSION_DENIED">>}}; + true -> + Oui = RouteListReq#iot_config_route_list_req_v1_pb.oui, + TestRoutes = application:get_env(hpr, test_route_list, []), + OuiRoutes = lists:filter(fun(Route) -> hpr_route:oui(Route) == Oui end, TestRoutes), + RouteList = #iot_config_route_list_res_v1_pb{ + routes = OuiRoutes, + timestamp = erlang:system_time(millisecond), + signer = <<>>, + signature = <<>> + }, + {ok, RouteList, Ctx} + catch + A:B:C -> + ct:print("hpr test route service list route error: ~n~p~n~p~n~p", [A, B, C]), + {grpc_error, {grpcbox_stream:code_to_status(7), <<"PERMISSION_DENIED">>}} + end. get(_Ctx, _Msg) -> {grpc_error, {grpcbox_stream:code_to_status(12), <<"UNIMPLEMENTED">>}}.