Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact 74e3a1f207231c1e7cf4ec058c77426ab3c458b956fc75e045306dd3e3d988f2:


-module(nova_security_handler_tests).
-include_lib("eunit/include/eunit.hrl").
-include("../include/nova_router.hrl").

-define(SETUP, fun() -> setup() end).
-define(CLEANUP, fun(S) -> cleanup(S) end).

setup() ->
    Prev = nova_test_helper:setup_nova_env(),
    application:set_env(nova, dispatch_backend, persistent_term),
    Tree = nova_routing_trie:new(#{use_strict => false, convert_to_binary => true}),
    persistent_term:put(nova_dispatch, Tree),
    persistent_term:put(nova_use_stacktrace, false),
    meck:new(cowboy_req, [passthrough, no_link]),
    meck:expect(cowboy_req, reply, fun(Status, Req) ->
        Req#{resp_status_code => Status}
    end),
    meck:expect(cowboy_req, set_resp_headers, fun(Headers, Req) ->
        ExistingHeaders = maps:get(resp_headers, Req, #{}),
        Req#{resp_headers => maps:merge(ExistingHeaders, Headers)}
    end),
    meck:expect(cowboy_req, set_resp_body, fun(Body, Req) ->
        Req#{resp_body => Body}
    end),
    meck:expect(cowboy_req, set_resp_header, fun(Name, Value, Req) ->
        Headers = maps:get(resp_headers, Req, #{}),
        Req#{resp_headers => Headers#{Name => Value}}
    end),
    Prev.

cleanup(Prev) ->
    meck:unload(cowboy_req),
    persistent_term:erase(nova_dispatch),
    persistent_term:erase(nova_use_stacktrace),
    nova_test_helper:cleanup_nova_env(Prev).

req() ->
    nova_test_helper:mock_req(<<"GET">>, <<"/">>).

env() ->
    #{secure => false, app => test_app, controller_data => #{}}.

%%====================================================================
%% secure=false — passthrough
%%====================================================================

secure_false_passthrough_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        Env = env(),
        ?assertEqual({ok, Req, Env}, nova_security_handler:execute(Req, Env))
    end}.

%%====================================================================
%% Callback returns true
%%====================================================================

callback_true_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        Callback = fun(_) -> true end,
        Env = (env())#{secure => Callback},
        ?assertEqual({ok, Req, Env}, nova_security_handler:execute(Req, Env))
    end}.

%%====================================================================
%% Callback returns {true, AuthData}
%%====================================================================

callback_true_with_auth_data_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        AuthData = #{user_id => 42},
        Callback = fun(_) -> {true, AuthData} end,
        Env = (env())#{secure => Callback},
        {ok, Req1, _Env1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(AuthData, maps:get(auth_data, Req1))
    end}.

%%====================================================================
%% Callback returns {true, AuthData} for websocket handler
%%====================================================================

callback_true_auth_data_websocket_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        AuthData = #{user_id => 99},
        Callback = fun(_) -> {true, AuthData} end,
        Env = (env())#{secure => Callback,
                     cowboy_handler => nova_ws_handler,
                     arguments => #{existing => data}},
        {ok, _Req1, Env1} = nova_security_handler:execute(Req, Env),
        Args = maps:get(arguments, Env1),
        ?assertEqual(#{auth_data => AuthData}, maps:get(controller_data, Args)),
        ?assertEqual(data, maps:get(existing, Args))
    end}.

%%====================================================================
%% Callback returns {false, Headers} -> 401 stop
%%====================================================================

callback_false_headers_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        RespHeaders = #{<<"x-error">> => <<"denied">>},
        Callback = fun(_) -> {false, RespHeaders} end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(401, maps:get(resp_status_code, Req1)),
        ?assertEqual(<<"denied">>, maps:get(<<"x-error">>, maps:get(resp_headers, Req1)))
    end}.

%%====================================================================
%% Callback returns {false, 403, Headers} -> 403 stop
%%====================================================================

callback_false_custom_status_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        RespHeaders = #{<<"x-reason">> => <<"forbidden">>},
        Callback = fun(_) -> {false, 403, RespHeaders} end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(403, maps:get(resp_status_code, Req1)),
        ?assertEqual(<<"forbidden">>, maps:get(<<"x-reason">>, maps:get(resp_headers, Req1)))
    end}.

%%====================================================================
%% Callback returns {false, 403, Headers, Body} -> stop with body
%%====================================================================

callback_false_with_body_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        RespHeaders = #{<<"content-type">> => <<"text/plain">>},
        Body = <<"Access denied">>,
        Callback = fun(_) -> {false, 403, RespHeaders, Body} end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(403, maps:get(resp_status_code, Req1)),
        ?assertEqual(Body, maps:get(resp_body, Req1))
    end}.

%%====================================================================
%% Callback returns {redirect, "/login"} -> 302 stop
%%====================================================================

callback_redirect_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        Callback = fun(_) -> {redirect, "/login"} end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(302, maps:get(resp_status_code, Req1)),
        ?assertEqual(<<"/login">>, maps:get(<<"location">>, maps:get(resp_headers, Req1)))
    end}.

%%====================================================================
%% Callback returns unexpected value -> 401 stop
%%====================================================================

callback_unexpected_value_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        Callback = fun(_) -> something_weird end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(401, maps:get(resp_status_code, Req1))
    end}.

%%====================================================================
%% Callback crashes -> 500 stop (stacktrace disabled)
%%====================================================================

callback_crash_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        Req = req(),
        Callback = fun(_) -> error(boom) end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(500, maps:get(resp_status_code, Req1))
    end}.

%%====================================================================
%% Callback crashes with stacktrace enabled -> 500 stop
%%====================================================================

callback_crash_stacktrace_test_() ->
    {setup, ?SETUP, ?CLEANUP, fun() ->
        persistent_term:put(nova_use_stacktrace, true),
        Req = req(),
        Callback = fun(_) -> error(boom) end,
        Env = (env())#{secure => Callback},
        {stop, Req1} = nova_security_handler:execute(Req, Env),
        ?assertEqual(500, maps:get(resp_status_code, Req1)),
        ?assert(is_map(maps:get(crash_info, Req1))),
        CrashInfo = maps:get(crash_info, Req1),
        ?assert(is_list(maps:get(stacktrace, CrashInfo)))
    end}.

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.