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 11b063c7380bb56877e324b5e83725457bcff1945cf1e0e2f0fbb577cb24da39:


-module(nova_csrf_plugin_test).
-include_lib("eunit/include/eunit.hrl").

%%====================================================================
%% Token generation tests
%%====================================================================

generate_token_is_binary_test() ->
    Token = nova_csrf_plugin:generate_token(),
    ?assert(is_binary(Token)).

generate_token_correct_length_test() ->
    %% 32 bytes base64-encoded = 44 characters
    Token = nova_csrf_plugin:generate_token(),
    ?assertEqual(44, byte_size(Token)).

generate_token_unique_test() ->
    Token1 = nova_csrf_plugin:generate_token(),
    Token2 = nova_csrf_plugin:generate_token(),
    ?assertNotEqual(Token1, Token2).

%%====================================================================
%% constant_time_compare tests
%%====================================================================

constant_time_compare_equal_test() ->
    ?assert(nova_csrf_plugin:constant_time_compare(<<"abc">>, <<"abc">>)).

constant_time_compare_different_test() ->
    ?assertNot(nova_csrf_plugin:constant_time_compare(<<"abc">>, <<"def">>)).

constant_time_compare_different_length_test() ->
    ?assertNot(nova_csrf_plugin:constant_time_compare(<<"abc">>, <<"abcd">>)).

constant_time_compare_undefined_test() ->
    ?assertNot(nova_csrf_plugin:constant_time_compare(<<"abc">>, undefined)).

constant_time_compare_both_undefined_test() ->
    ?assertNot(nova_csrf_plugin:constant_time_compare(undefined, undefined)).

%%====================================================================
%% is_safe_method tests
%%====================================================================

is_safe_method_get_test() ->
    ?assert(nova_csrf_plugin:is_safe_method(<<"GET">>)).

is_safe_method_head_test() ->
    ?assert(nova_csrf_plugin:is_safe_method(<<"HEAD">>)).

is_safe_method_options_test() ->
    ?assert(nova_csrf_plugin:is_safe_method(<<"OPTIONS">>)).

is_safe_method_post_test() ->
    ?assertNot(nova_csrf_plugin:is_safe_method(<<"POST">>)).

is_safe_method_put_test() ->
    ?assertNot(nova_csrf_plugin:is_safe_method(<<"PUT">>)).

is_safe_method_patch_test() ->
    ?assertNot(nova_csrf_plugin:is_safe_method(<<"PATCH">>)).

is_safe_method_delete_test() ->
    ?assertNot(nova_csrf_plugin:is_safe_method(<<"DELETE">>)).

%%====================================================================
%% is_excluded_path tests
%%====================================================================

is_excluded_path_match_test() ->
    ?assert(nova_csrf_plugin:is_excluded_path(<<"/api/webhooks/stripe">>, [<<"/api/webhooks">>])).

is_excluded_path_no_match_test() ->
    ?assertNot(nova_csrf_plugin:is_excluded_path(<<"/admin/settings">>, [<<"/api/webhooks">>])).

is_excluded_path_empty_list_test() ->
    ?assertNot(nova_csrf_plugin:is_excluded_path(<<"/anything">>, [])).

is_excluded_path_exact_match_test() ->
    ?assert(nova_csrf_plugin:is_excluded_path(<<"/api">>, [<<"/api">>])).

is_excluded_path_multiple_prefixes_test() ->
    ?assert(nova_csrf_plugin:is_excluded_path(<<"/health">>, [<<"/api">>, <<"/health">>])).

%%====================================================================
%% get_submitted_token tests
%%====================================================================

get_submitted_token_from_header_test() ->
    Req = #{headers => #{<<"x-csrf-token">> => <<"header-token">>}},
    ?assertEqual(<<"header-token">>,
                 nova_csrf_plugin:get_submitted_token(Req, <<"_csrf_token">>, <<"x-csrf-token">>)).

get_submitted_token_from_params_test() ->
    %% No header present — falls through to params
    Req = #{headers => #{}, params => #{<<"_csrf_token">> => <<"form-token">>}},
    ?assertEqual(<<"form-token">>,
                 nova_csrf_plugin:get_submitted_token(Req, <<"_csrf_token">>, <<"x-csrf-token">>)).

get_submitted_token_header_priority_test() ->
    Req = #{headers => #{<<"x-csrf-token">> => <<"header-token">>},
            params => #{<<"_csrf_token">> => <<"form-token">>}},
    ?assertEqual(<<"header-token">>,
                 nova_csrf_plugin:get_submitted_token(Req, <<"_csrf_token">>, <<"x-csrf-token">>)).

get_submitted_token_missing_test() ->
    Req = #{headers => #{}},
    ?assertEqual(undefined,
                 nova_csrf_plugin:get_submitted_token(Req, <<"_csrf_token">>, <<"x-csrf-token">>)).

%%====================================================================
%% pre_request integration tests (using meck)
%%====================================================================

safe_method_with_existing_session_token_test() ->
    setup_meck([nova_session]),
    try
        Token = <<"existing-token">>,
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, Token} end),
        Req = #{method => <<"GET">>, path => <<"/">>},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{}),
        ?assertEqual(Token, maps:get(csrf_token, Req1))
    after
        cleanup_meck([nova_session])
    end.

safe_method_generates_token_when_missing_test() ->
    setup_meck([nova_session]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {error, not_found} end),
        meck:expect(nova_session, set, fun(_Req, <<"_csrf_token">>, _Token) -> ok end),
        Req = #{method => <<"GET">>, path => <<"/">>},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{}),
        ?assert(maps:is_key(csrf_token, Req1)),
        ?assert(is_binary(maps:get(csrf_token, Req1)))
    after
        cleanup_meck([nova_session])
    end.

safe_method_no_session_proceeds_without_token_test() ->
    setup_meck([nova_session]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {error, not_found} end),
        meck:expect(nova_session, set, fun(_Req, <<"_csrf_token">>, _Token) -> {error, session_id_not_set} end),
        Req = #{method => <<"GET">>, path => <<"/">>},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{}),
        ?assertNot(maps:is_key(csrf_token, Req1))
    after
        cleanup_meck([nova_session])
    end.

unsafe_method_valid_token_from_header_test() ->
    setup_meck([nova_session, cowboy_req]),
    try
        Token = <<"valid-token">>,
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, Token} end),
        meck:expect(cowboy_req, header, fun(<<"x-csrf-token">>, _Req) -> Token end),
        Req = #{method => <<"POST">>, path => <<"/submit">>,
                headers => #{<<"x-csrf-token">> => Token}},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{}),
        ?assertEqual(Token, maps:get(csrf_token, Req1))
    after
        cleanup_meck([cowboy_req, nova_session])
    end.

unsafe_method_valid_token_from_params_test() ->
    setup_meck([nova_session, cowboy_req]),
    try
        Token = <<"valid-token">>,
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, Token} end),
        meck:expect(cowboy_req, header, fun(<<"x-csrf-token">>, _Req) -> undefined end),
        Req = #{method => <<"POST">>, path => <<"/submit">>,
                params => #{<<"_csrf_token">> => Token}},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{}),
        ?assertEqual(Token, maps:get(csrf_token, Req1))
    after
        cleanup_meck([cowboy_req, nova_session])
    end.

unsafe_method_invalid_token_test() ->
    setup_meck([nova_session, cowboy_req]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, <<"real-token0">>} end),
        meck:expect(cowboy_req, header, fun(<<"x-csrf-token">>, _Req) -> <<"wrong-token">> end),
        Req = #{method => <<"POST">>, path => <<"/submit">>,
                headers => #{<<"x-csrf-token">> => <<"wrong-token">>}},
        {stop, {reply, 403, _, _}, _Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{})
    after
        cleanup_meck([cowboy_req, nova_session])
    end.

unsafe_method_no_session_test() ->
    setup_meck([nova_session]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {error, not_found} end),
        Req = #{method => <<"POST">>, path => <<"/submit">>},
        {stop, {reply, 403, _, _}, _Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{})
    after
        cleanup_meck([nova_session])
    end.

unsafe_method_missing_submitted_token_test() ->
    setup_meck([nova_session, cowboy_req]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, <<"real-token">>} end),
        meck:expect(cowboy_req, header, fun(<<"x-csrf-token">>, _Req) -> undefined end),
        Req = #{method => <<"DELETE">>, path => <<"/resource/1">>},
        {stop, {reply, 403, _, _}, _Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, #{}, #{})
    after
        cleanup_meck([cowboy_req, nova_session])
    end.

excluded_path_skips_validation_test() ->
    setup_meck([nova_session]),
    try
        meck:expect(nova_session, get, fun(_Req, <<"_csrf_token">>) -> {ok, <<"token">>} end),
        Req = #{method => <<"POST">>, path => <<"/api/webhooks/stripe">>},
        Options = #{excluded_paths => [<<"/api/webhooks">>]},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, Options, #{}),
        ?assertEqual(<<"token">>, maps:get(csrf_token, Req1))
    after
        cleanup_meck([nova_session])
    end.

custom_field_name_test() ->
    setup_meck([nova_session, cowboy_req]),
    try
        Token = <<"custom-token">>,
        meck:expect(nova_session, get, fun(_Req, <<"csrf">>) -> {ok, Token} end),
        meck:expect(cowboy_req, header, fun(<<"x-my-csrf">>, _Req) -> undefined end),
        Req = #{method => <<"POST">>, path => <<"/submit">>,
                params => #{<<"my_csrf">> => Token}},
        Options = #{field_name => <<"my_csrf">>,
                    header_name => <<"x-my-csrf">>,
                    session_key => <<"csrf">>},
        {ok, Req1, _State} = nova_csrf_plugin:pre_request(Req, #{}, Options, #{}),
        ?assertEqual(Token, maps:get(csrf_token, Req1))
    after
        cleanup_meck([cowboy_req, nova_session])
    end.

%%====================================================================
%% Helpers
%%====================================================================

setup_meck(Modules) ->
    [meck:new(M, [passthrough]) || M <- Modules].

cleanup_meck(Modules) ->
    [meck:unload(M) || M <- Modules].

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.