Просмотр исходного кода

refactor(bridges): add POST /bridges for creating

Shawn 4 лет назад
Родитель
Сommit
4dac90f4a7

+ 5 - 3
apps/emqx_bridge/src/emqx_bridge.erl

@@ -65,7 +65,8 @@ load_hook() ->
         end, maps:to_list(Bridges)).
 
 load_hook(#{from_local_topic := _}) ->
-    emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []});
+    emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}),
+    ok;
 load_hook(_Conf) -> ok.
 
 unload_hook() ->
@@ -98,12 +99,13 @@ bridge_type(emqx_connector_http) -> http.
 post_config_update(_Req, NewConf, OldConf, _AppEnv) ->
     #{added := Added, removed := Removed, changed := Updated}
         = diff_confs(NewConf, OldConf),
-    perform_bridge_changes([
+    Result = perform_bridge_changes([
         {fun remove/3, Removed},
         {fun create/3, Added},
         {fun update/3, Updated}
     ]),
-    reload_hook().
+    ok = reload_hook(),
+    Result.
 
 perform_bridge_changes(Tasks) ->
     perform_bridge_changes(Tasks, ok).

+ 176 - 35
apps/emqx_bridge/src/emqx_bridge_api.erl

@@ -19,23 +19,40 @@
 
 -export([api_spec/0]).
 
--export([ list_bridges/2
+-export([ list_create_bridges_in_cluster/2
         , list_local_bridges/1
-        , crud_bridges_cluster/2
-        , crud_bridges/3
+        , crud_bridges_in_cluster/2
+        , crud_local_bridges/4
         , manage_bridges/2
         ]).
 
--define(TYPES, [mqtt]).
--define(BRIDGE(N, T, C), #{<<"id">> => N, <<"type">> => T, <<"config">> => C}).
+-define(TYPES, [mqtt, http]).
 -define(TRY_PARSE_ID(ID, EXPR),
     try emqx_bridge:parse_bridge_id(Id) of
         {BridgeType, BridgeName} -> EXPR
     catch
         error:{invalid_bridge_id, Id0} ->
-            {400, #{code => 102, message => <<"invalid_bridge_id: ", Id0/binary>>}}
+            {400, #{code => 'INVALID_ID', message => <<"invalid_bridge_id: ", Id0/binary,
+                ". Bridge Ids must be of format <bridge_type>:<name>">>}}
     end).
 
+-define(METRICS(SUCC, FAILED, RATE, RATE_5, RATE_MAX),
+    #{
+        success => SUCC,
+        failed => FAILED,
+        rate => RATE,
+        rate_last5m => RATE_5,
+        rate_max => RATE_MAX
+    }).
+-define(metrics(SUCC, FAILED, RATE, RATE_5, RATE_MAX),
+    #{
+        success := SUCC,
+        failed := FAILED,
+        rate := RATE,
+        rate_last5m := RATE_5,
+        rate_max := RATE_MAX
+    }).
+
 req_schema() ->
     Schema = [
         case maps:to_list(emqx:get_raw_config([bridges, T], #{})) of
@@ -47,14 +64,50 @@ req_schema() ->
      || T <- ?TYPES],
     #{'oneOf' => Schema}.
 
+node_schema() ->
+    #{type => string, example => "emqx@127.0.0.1"}.
+
+status_schema() ->
+    #{type => string, enum => [connected, disconnected]}.
+
+metrics_schema() ->
+    #{ type => object
+     , properties => #{
+           success => #{type => integer, example => "0"},
+           failed => #{type => integer, example => "0"},
+           rate => #{type => number, format => float, example => "0.0"},
+           rate_last5m => #{type => number, format => float, example => "0.0"},
+           rate_max => #{type => number, format => float, example => "0.0"}
+       }
+    }.
+
+per_node_schema(Key, Schema) ->
+    #{
+        type => array,
+        items => #{
+            type => object,
+            properties => #{
+                node => node_schema(),
+                Key => Schema
+            }
+        }
+    }.
+
 resp_schema() ->
-    #{'oneOf' := Schema} = req_schema(),
     AddMetadata = fun(Prop) ->
-        Prop#{status => #{type => string, enum => [connected, disconnected, connecting]},
-              id => #{type => string},
+        Prop#{status => status_schema(),
+              node_status => per_node_schema(status, status_schema()),
+              metrics => metrics_schema(),
+              node_metrics => per_node_schema(metrics, metrics_schema()),
+              id => #{type => string, example => "http:my_http_bridge"},
               bridge_type => #{type => string, enum => ?TYPES},
-              node => #{type => string}}
+              node => node_schema()
+            }
     end,
+    more_props_resp_schema(AddMetadata).
+
+more_props_resp_schema(AddMetadata) ->
+    #{oneOf := Schema} = req_schema(),
     Schema1 = [S#{properties => AddMetadata(Prop)}
                || S = #{properties := Prop} <- Schema],
     #{'oneOf' => Schema1}.
@@ -66,6 +119,10 @@ bridge_apis() ->
     [list_all_bridges_api(), crud_bridges_apis(), operation_apis()].
 
 list_all_bridges_api() ->
+    ReqSchema = more_props_resp_schema(fun(Prop) ->
+        Prop#{id => #{type => string, required => true}}
+    end),
+    RespSchema = resp_schema(),
     Metadata = #{
         get => #{
             description => <<"List all created bridges">>,
@@ -73,9 +130,18 @@ list_all_bridges_api() ->
                 <<"200">> => emqx_mgmt_util:array_schema(resp_schema(),
                     <<"A list of the bridges">>)
             }
+        },
+        post => #{
+            description => <<"Create a new bridge">>,
+            'requestBody' => emqx_mgmt_util:schema(ReqSchema),
+            responses => #{
+                <<"201">> => emqx_mgmt_util:schema(RespSchema, <<"Bridge created">>),
+                <<"400">> => emqx_mgmt_util:error_schema(<<"Create bridge failed">>,
+                    ['UPDATE_FAILED'])
+            }
         }
     },
-    {"/bridges/", Metadata, list_bridges}.
+    {"/bridges/", Metadata, list_create_bridges_in_cluster}.
 
 crud_bridges_apis() ->
     ReqSchema = req_schema(),
@@ -91,7 +157,7 @@ crud_bridges_apis() ->
             }
         },
         put => #{
-            description => <<"Create or update a bridge">>,
+            description => <<"Update a bridge">>,
             parameters => [param_path_id()],
             'requestBody' => emqx_mgmt_util:schema(ReqSchema),
             responses => #{
@@ -109,7 +175,7 @@ crud_bridges_apis() ->
             }
         }
     },
-    {"/bridges/:id", Metadata, crud_bridges_cluster}.
+    {"/bridges/:id", Metadata, crud_bridges_in_cluster}.
 
 operation_apis() ->
     Metadata = #{
@@ -153,53 +219,73 @@ param_path_operation()->
         example => restart
     }.
 
-list_bridges(get, _Params) ->
-    {200, lists:append([list_local_bridges(Node) || Node <- mria_mnesia:running_nodes()])}.
+list_create_bridges_in_cluster(post, #{body := #{<<"id">> := Id} = Conf}) ->
+    crud_bridges_in_cluster(post, Id, maps:remove(<<"id">>, Conf));
+list_create_bridges_in_cluster(get, _Params) ->
+    {200, zip_bridges([list_local_bridges(Node) || Node <- mria_mnesia:running_nodes()])}.
 
 list_local_bridges(Node) when Node =:= node() ->
     [format_resp(Data) || Data <- emqx_bridge:list()];
 list_local_bridges(Node) ->
     rpc_call(Node, list_local_bridges, [Node]).
 
-crud_bridges_cluster(Method, Params) ->
-    Results = [crud_bridges(Node, Method, Params) || Node <- mria_mnesia:running_nodes()],
-    case lists:filter(fun({200}) -> false; ({200, _}) -> false; (_) -> true end, Results) of
+crud_bridges_in_cluster(Method, #{bindings := #{id := Id}, body := Body}) ->
+    crud_bridges_in_cluster(Method, Id, Body).
+
+crud_bridges_in_cluster(Method, Id, Body) ->
+    Results = [crud_local_bridges(Node, Method, Id, Body) || Node <- mria_mnesia:running_nodes()],
+    Filter = fun ({200}) -> false;
+                 ({Code, _}) when Code == 200; Code == 201 -> false;
+                 (_) -> true
+             end,
+    case lists:filter(Filter, Results) of
         [] ->
             case Results of
                 [{200} | _] -> {200};
-                _ -> {200, [Res || {200, Res} <- Results]}
+                [{Code, _} | _] when Code == 200; Code == 201 ->
+                    {Code, format_bridge_info([Bridge || {_, Bridge} <- Results])}
             end;
         Errors ->
             hd(Errors)
     end.
 
-crud_bridges(Node, Method, Params) when Node =/= node() ->
-    rpc_call(Node, crud_bridges, [Node, Method, Params]);
+crud_local_bridges(Node, Method, Id, Body) when Node =/= node() ->
+    rpc_call(Node, crud_local_bridges, [Node, Method, Id, Body]);
 
-crud_bridges(_, get, #{bindings := #{id := Id}}) ->
+crud_local_bridges(_, get, Id, _Body) ->
     ?TRY_PARSE_ID(Id, case emqx_bridge:lookup(BridgeType, BridgeName) of
         {ok, Data} -> {200, format_resp(Data)};
         {error, not_found} ->
             {404, #{code => 102, message => <<"not_found: ", Id/binary>>}}
     end);
 
-crud_bridges(_, put, #{bindings := #{id := Id}, body := Conf}) ->
+crud_local_bridges(_, post, Id, Conf) ->
     ?TRY_PARSE_ID(Id,
-        case emqx:update_config(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], Conf,
-                #{rawconf_with_defaults => true}) of
-            {ok, #{raw_config := RawConf, post_config_update := #{emqx_bridge := Data}}} ->
-                {200, format_resp(#{id => Id, raw_config => RawConf, resource_data => Data})};
-            {ok, _} -> %% the bridge already exits
-                {ok, Data} = emqx_bridge:lookup(BridgeType, BridgeName),
-                {200, format_resp(Data)};
-            {error, Reason} ->
-                {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
+        case emqx_bridge:lookup(BridgeType, BridgeName) of
+            {ok, _} -> {400, #{code => 'ALREADY_EXISTS', message => <<"bridge already exists">>}};
+            {error, not_found} ->
+                case ensure_bridge(Id, BridgeType, BridgeName, Conf) of
+                    {ok, Resp} -> {201, Resp};
+                    {error, Error} -> {400, Error}
+                end
         end);
 
-crud_bridges(_, delete, #{bindings := #{id := Id}}) ->
+crud_local_bridges(_, put, Id, Conf) ->
+    ?TRY_PARSE_ID(Id,
+        case emqx_bridge:lookup(BridgeType, BridgeName) of
+            {ok, _} ->
+                case ensure_bridge(Id, BridgeType, BridgeName, Conf) of
+                    {ok, Resp} -> {200, Resp};
+                    {error, Error} -> {400, Error}
+                end;
+            {error, not_found} ->
+                {404, #{code => 'NOT_FOUND', message => <<"bridge not found">>}}
+        end);
+
+crud_local_bridges(_, delete, Id, _Body) ->
     ?TRY_PARSE_ID(Id,
         case emqx:remove_config(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName]) of
-            {ok, _} -> {200};
+            {ok, _} -> {204};
             {error, Reason} ->
                 {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
         end).
@@ -218,13 +304,68 @@ manage_bridges(post, #{bindings := #{node := Node, id := Id, operation := Op}})
                 {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
         end).
 
+ensure_bridge(Id, BridgeType, BridgeName, Conf) ->
+    case emqx:update_config(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], Conf,
+            #{rawconf_with_defaults => true}) of
+        {ok, #{raw_config := RawConf, post_config_update := #{emqx_bridge := Data}}} ->
+            {ok, format_resp(#{id => Id, raw_config => RawConf, resource_data => Data})};
+        {error, Reason} ->
+            {error, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
+    end.
+
+zip_bridges([BridgesFirstNode | _] = BridgesAllNodes) ->
+    lists:foldl(fun(#{id := Id}, Acc) ->
+            Bridges = pick_bridges_by_id(Id, BridgesAllNodes),
+            [format_bridge_info(Bridges) | Acc]
+        end, [], BridgesFirstNode).
+
+pick_bridges_by_id(Id, BridgesAllNodes) ->
+    lists:foldl(fun(BridgesOneNode, Acc) ->
+            [BridgeInfo] = [Bridge || Bridge = #{id := Id0} <- BridgesOneNode, Id0 == Id],
+            [BridgeInfo | Acc]
+        end, [], BridgesAllNodes).
+
+format_bridge_info([FirstBridge | _] = Bridges) ->
+    Res = maps:remove(node, FirstBridge),
+    NodeStatus = collect_status(Bridges),
+    NodeMetrics = collect_metrics(Bridges),
+    Res#{ status => aggregate_status(NodeStatus)
+        , node_status => NodeStatus
+        , metrics => aggregate_metrics(NodeMetrics)
+        , node_metrics => NodeMetrics
+        }.
+
+collect_status(Bridges) ->
+    [maps:with([node, status], B) || B <- Bridges].
+
+aggregate_status(AllStatus) ->
+    AllConnected = lists:all(fun (#{status := connected}) -> true;
+                                 (_) -> false
+                             end, AllStatus),
+    case AllConnected of
+        true -> connected;
+        false -> disconnected
+    end.
+
+collect_metrics(Bridges) ->
+    [maps:with([node, metrics], B) || B <- Bridges].
+
+aggregate_metrics(AllMetrics) ->
+    InitMetrics = ?METRICS(0,0,0,0,0),
+    lists:foldl(fun(#{metrics := ?metrics(Succ1, Failed1, Rate1, Rate5m1, RateMax1)},
+                    ?metrics(Succ0, Failed0, Rate0, Rate5m0, RateMax0)) ->
+            ?METRICS(Succ1 + Succ0, Failed1 + Failed0,
+                     Rate1 + Rate0, Rate5m1 + Rate5m0, RateMax1 + RateMax0)
+        end, InitMetrics, AllMetrics).
+
 format_resp(#{id := Id, raw_config := RawConf, resource_data := #{mod := Mod, status := Status}}) ->
     IsConnected = fun(started) -> connected; (_) -> disconnected end,
     RawConf#{
         id => Id,
         node => node(),
         bridge_type => emqx_bridge:bridge_type(Mod),
-        status => IsConnected(Status)
+        status => IsConnected(Status),
+        metrics => ?METRICS(0,0,0,0,0)
     }.
 
 rpc_call(Node, Fun, Args) ->

+ 108 - 77
apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl

@@ -21,6 +21,22 @@
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("common_test/include/ct.hrl").
 -define(CONF_DEFAULT, <<"bridges: {}">>).
+-define(TEST_ID, <<"http:test_bridge">>).
+-define(URL(PORT, PATH), list_to_binary(
+    io_lib:format("http://localhost:~s/~s",
+                  [integer_to_list(PORT), PATH]))).
+-define(HTTP_BRIDGE(URL),
+#{
+    <<"url">> => URL,
+    <<"from_local_topic">> => <<"emqx_http/#">>,
+    <<"method">> => <<"post">>,
+    <<"ssl">> => #{<<"enable">> => false},
+    <<"body">> => <<"${payload}">>,
+    <<"headers">> => #{
+        <<"content-type">> => <<"application/json">>
+    }
+
+}).
 
 all() ->
     emqx_common_test_helpers:all(?MODULE).
@@ -56,23 +72,6 @@ init_per_testcase(_, Config) ->
 end_per_testcase(_, _Config) ->
     ok.
 
--define(URL(PORT, PATH), list_to_binary(
-    io_lib:format("http://localhost:~s/~s",
-                  [integer_to_list(PORT), PATH]))).
-
--define(HTTP_BRIDGE(URL),
-#{
-    <<"url">> => URL,
-    <<"from_local_topic">> => <<"emqx_http/#">>,
-    <<"method">> => <<"post">>,
-    <<"ssl">> => #{<<"enable">> => false},
-    <<"body">> => <<"${payload}">>,
-    <<"headers">> => #{
-        <<"content-type">> => <<"application/json">>
-    }
-
-}).
-
 %%------------------------------------------------------------------------------
 %% HTTP server for testing
 %%------------------------------------------------------------------------------
@@ -124,105 +123,137 @@ handle_fun_200_ok(Conn) ->
 %% Testcases
 %%------------------------------------------------------------------------------
 
-t_crud_apis(_) ->
+t_http_crud_apis(_) ->
     Port = start_http_server(fun handle_fun_200_ok/1),
     %% assert we there's no bridges at first
     {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
 
-    %% then we add a http bridge now
-    %% PUT /bridges/:id will create or update a bridge
+    %% then we add a http bridge, using PUT
+    %% POST /bridges/ will create a bridge
     URL1 = ?URL(Port, "path1"),
-    {ok, 200, Bridge} = request(put, uri(["bridges", "http:test_bridge"]),
-                                ?HTTP_BRIDGE(URL1)),
+    {ok, 201, Bridge} = request(post, uri(["bridges"]),
+                                ?HTTP_BRIDGE(URL1)#{<<"id">> => ?TEST_ID}),
+
     %ct:pal("---bridge: ~p", [Bridge]),
-    ?assertMatch([ #{ <<"id">> := <<"http:test_bridge">>
-                    , <<"bridge_type">> := <<"http">>
-                    , <<"status">> := _
-                    , <<"node">> := _
-                    , <<"url">> := URL1
-                    }], jsx:decode(Bridge)),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"bridge_type">> := <<"http">>
+                  , <<"status">> := _
+                  , <<"node_status">> := [_|_]
+                  , <<"metrics">> := _
+                  , <<"node_metrics">> := [_|_]
+                  , <<"url">> := URL1
+                  }, jsx:decode(Bridge)),
+
+    %% create a again returns an error
+    {ok, 400, RetMsg} = request(post, uri(["bridges"]),
+                                ?HTTP_BRIDGE(URL1)#{<<"id">> => ?TEST_ID}),
+    ?assertMatch(
+        #{ <<"code">> := _
+         , <<"message">> := <<"bridge already exists">>
+         }, jsx:decode(RetMsg)),
 
     %% update the request-path of the bridge
     URL2 = ?URL(Port, "path2"),
-    {ok, 200, Bridge2} = request(put, uri(["bridges", "http:test_bridge"]),
+    {ok, 200, Bridge2} = request(put, uri(["bridges", ?TEST_ID]),
                                  ?HTTP_BRIDGE(URL2)),
-    ?assertMatch([ #{ <<"id">> := <<"http:test_bridge">>
-                    , <<"bridge_type">> := <<"http">>
-                    , <<"status">> := _
-                    , <<"node">> := _
-                    , <<"url">> := URL2
-                    }], jsx:decode(Bridge2)),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"bridge_type">> := <<"http">>
+                  , <<"status">> := _
+                  , <<"node_status">> := [_|_]
+                  , <<"metrics">> := _
+                  , <<"node_metrics">> := [_|_]
+                  , <<"url">> := URL2
+                  }, jsx:decode(Bridge2)),
 
     %% list all bridges again, assert Bridge2 is in it
     {ok, 200, Bridge2Str} = request(get, uri(["bridges"]), []),
-    ?assertMatch([ #{ <<"id">> := <<"http:test_bridge">>
-                    , <<"bridge_type">> := <<"http">>
-                    , <<"status">> := _
-                    , <<"node">> := _
-                    , <<"url">> := URL2
-                    }], jsx:decode(Bridge2Str)),
+    ?assertMatch([#{ <<"id">> := ?TEST_ID
+                   , <<"bridge_type">> := <<"http">>
+                   , <<"status">> := _
+                   , <<"node_status">> := [_|_]
+                   , <<"metrics">> := _
+                   , <<"node_metrics">> := [_|_]
+                   , <<"url">> := URL2
+                   }], jsx:decode(Bridge2Str)),
 
     %% get the bridge by id
-    {ok, 200, Bridge3Str} = request(get, uri(["bridges", "http:test_bridge"]), []),
-    ?assertMatch([#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"bridge_type">> := <<"http">>
-                    , <<"status">> := _
-                    , <<"node">> := _
-                    , <<"url">> := URL2
-                    }], jsx:decode(Bridge3Str)),
+    {ok, 200, Bridge3Str} = request(get, uri(["bridges", ?TEST_ID]), []),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"bridge_type">> := <<"http">>
+                  , <<"status">> := _
+                  , <<"node_status">> := [_|_]
+                  , <<"metrics">> := _
+                  , <<"node_metrics">> := [_|_]
+                  , <<"url">> := URL2
+                  }, jsx:decode(Bridge3Str)),
 
     %% delete the bridge
-    {ok,200,<<>>} = request(delete, uri(["bridges", "http:test_bridge"]), []),
+    {ok, 204, <<>>} = request(delete, uri(["bridges", ?TEST_ID]), []),
     {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
+
+    %% update a deleted bridge returns an error
+    {ok, 404, ErrMsg2} = request(put, uri(["bridges", ?TEST_ID]),
+                                 ?HTTP_BRIDGE(URL2)),
+    ?assertMatch(
+        #{ <<"code">> := _
+         , <<"message">> := <<"bridge not found">>
+         }, jsx:decode(ErrMsg2)),
     ok.
 
 t_start_stop_bridges(_) ->
     Port = start_http_server(fun handle_fun_200_ok/1),
     URL1 = ?URL(Port, "abc"),
-    {ok, 200, Bridge} = request(put, uri(["bridges", "http:test_bridge"]), ?HTTP_BRIDGE(URL1)),
+    {ok, 201, Bridge} = request(post, uri(["bridges"]),
+                                ?HTTP_BRIDGE(URL1)#{<<"id">> => ?TEST_ID}),
     %ct:pal("the bridge ==== ~p", [Bridge]),
-    ?assertMatch( [#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"bridge_type">> := <<"http">>
-                    , <<"status">> := <<"connected">>
-                    , <<"node">> := _
-                    , <<"url">> := URL1
-                    }], jsx:decode(Bridge)),
+    ?assertMatch(
+        #{ <<"id">> := ?TEST_ID
+         , <<"bridge_type">> := <<"http">>
+         , <<"status">> := _
+         , <<"node_status">> := [_|_]
+         , <<"metrics">> := _
+         , <<"node_metrics">> := [_|_]
+         , <<"url">> := URL1
+         }, jsx:decode(Bridge)),
     %% stop it
     {ok, 200, <<>>} = request(post,
-        uri(["nodes", node(), "bridges", "http:test_bridge", "operation", "stop"]),
+        uri(["nodes", node(), "bridges", ?TEST_ID, "operation", "stop"]),
         <<"">>),
-    {ok, 200, Bridge2} = request(get, uri(["bridges", "http:test_bridge"]), []),
-    ?assertMatch([#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"status">> := <<"disconnected">>
-                    }], jsx:decode(Bridge2)),
+    {ok, 200, Bridge2} = request(get, uri(["bridges", ?TEST_ID]), []),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"status">> := <<"disconnected">>
+                  }, jsx:decode(Bridge2)),
     %% start again
     {ok, 200, <<>>} = request(post,
-        uri(["nodes", node(), "bridges", "http:test_bridge", "operation", "start"]),
+        uri(["nodes", node(), "bridges", ?TEST_ID, "operation", "start"]),
         <<"">>),
-    {ok, 200, Bridge3} = request(get, uri(["bridges", "http:test_bridge"]), []),
-    ?assertMatch([#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"status">> := <<"connected">>
-                    }], jsx:decode(Bridge3)),
+    {ok, 200, Bridge3} = request(get, uri(["bridges", ?TEST_ID]), []),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"status">> := <<"connected">>
+                  }, jsx:decode(Bridge3)),
     %% restart an already started bridge
     {ok, 200, <<>>} = request(post,
-        uri(["nodes", node(), "bridges", "http:test_bridge", "operation", "restart"]),
+        uri(["nodes", node(), "bridges", ?TEST_ID, "operation", "restart"]),
         <<"">>),
-    {ok, 200, Bridge3} = request(get, uri(["bridges", "http:test_bridge"]), []),
-    ?assertMatch([#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"status">> := <<"connected">>
-                    }], jsx:decode(Bridge3)),
+    {ok, 200, Bridge3} = request(get, uri(["bridges", ?TEST_ID]), []),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"status">> := <<"connected">>
+                  }, jsx:decode(Bridge3)),
     %% stop it again
     {ok, 200, <<>>} = request(post,
-        uri(["nodes", node(), "bridges", "http:test_bridge", "operation", "stop"]),
+        uri(["nodes", node(), "bridges", ?TEST_ID, "operation", "stop"]),
         <<"">>),
     %% restart a stopped bridge
     {ok, 200, <<>>} = request(post,
-        uri(["nodes", node(), "bridges", "http:test_bridge", "operation", "restart"]),
+        uri(["nodes", node(), "bridges", ?TEST_ID, "operation", "restart"]),
         <<"">>),
-    {ok, 200, Bridge4} = request(get, uri(["bridges", "http:test_bridge"]), []),
-    ?assertMatch([#{ <<"id">> := <<"http:test_bridge">>
-                    , <<"status">> := <<"connected">>
-                    }], jsx:decode(Bridge4)).
+    {ok, 200, Bridge4} = request(get, uri(["bridges", ?TEST_ID]), []),
+    ?assertMatch(#{ <<"id">> := ?TEST_ID
+                  , <<"status">> := <<"connected">>
+                  }, jsx:decode(Bridge4)),
+    %% delete the bridge
+    {ok, 204, <<>>} = request(delete, uri(["bridges", ?TEST_ID]), []),
+    {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
 
 %%--------------------------------------------------------------------
 %% HTTP Request