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 5d15ab0d3b7214a7479561b594bce915d773f117cd61e8493f42f6fb7344e538:


-module(nova_session_tests).
-include_lib("eunit/include/eunit.hrl").
-include("../include/nova_pubsub.hrl").

%%====================================================================
%% Integration tests for nova_session + nova_session_ets
%%
%% These tests start the real nova_session_ets gen_server with pg-backed
%% pubsub, exercising the full set/get/delete path through ETS.
%%====================================================================

%%--------------------------------------------------------------------
%% Setup / Cleanup
%%--------------------------------------------------------------------

setup() ->
    pg:start_link(nova_scope),
    application:set_env(nova, bootstrap_application, nova),
    application:set_env(nova, use_sessions, true),
    application:set_env(nova, session_manager, nova_session_ets),
    {ok, Pid} = nova_session_ets:start_link(),
    meck:new(cowboy_req, [non_strict]),
    meck:expect(cowboy_req, match_cookies,
                fun([{session_id, [], undefined}], Req) ->
                    SId = maps:get(cookie_session_id, Req, undefined),
                    #{session_id => SId}
                end),
    meck:expect(cowboy_req, set_resp_cookie,
                fun(Name, Value, Req, Opts) ->
                    Req#{resp_cookie => #{name => Name, value => Value, opts => Opts}}
                end),
    Pid.

cleanup(Pid) ->
    meck:unload(cowboy_req),
    gen_server:stop(Pid),
    application:unset_env(nova, bootstrap_application),
    application:unset_env(nova, use_sessions),
    application:unset_env(nova, session_manager),
    ok.

%% After a pubsub broadcast, do a gen_server:call to synchronize
sync() ->
    nova_session_ets:get_value(<<"__sync__">>, <<"__sync__">>).

req_with_session(SessionId) ->
    #{nova_session_id => SessionId}.

req_with_cookie(SessionId) ->
    #{cookie_session_id => SessionId}.

%%====================================================================
%% Test generators
%%====================================================================

nova_session_ets_test_() ->
    {foreach,
     fun setup/0,
     fun cleanup/1,
     [
      fun set_and_get_value/0,
      fun get_missing_session/0,
      fun get_missing_key/0,
      fun set_multiple_keys/0,
      fun delete_session/0,
      fun delete_single_key/0,
      fun cookie_fallback/0,
      fun no_session_returns_error/0,
      fun no_session_delete_returns_ok/0,
      fun sessions_disabled_throws/0,
      fun delete_sets_expired_cookie/0,
      fun delete_key_preserves_other_keys/0,
      fun set_overwrites_existing_key/0,
      fun generate_session_id_format/0
     ]}.

%%====================================================================
%% Tests
%%====================================================================

set_and_get_value() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"username">>, <<"alice">>),
    sync(),
    ?assertEqual({ok, <<"alice">>}, nova_session:get(Req, <<"username">>)).

get_missing_session() ->
    Req = #{},
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"key">>)).

get_missing_key() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"exists">>, <<"yes">>),
    sync(),
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"missing">>)).

set_multiple_keys() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"k1">>, <<"v1">>),
    ok = nova_session:set(Req, <<"k2">>, <<"v2">>),
    sync(),
    ?assertEqual({ok, <<"v1">>}, nova_session:get(Req, <<"k1">>)),
    ?assertEqual({ok, <<"v2">>}, nova_session:get(Req, <<"k2">>)).

delete_session() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"key">>, <<"val">>),
    sync(),
    {ok, _Req1} = nova_session:delete(Req),
    sync(),
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"key">>)).

delete_single_key() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"keep">>, <<"yes">>),
    ok = nova_session:set(Req, <<"remove">>, <<"bye">>),
    sync(),
    {ok, _Req1} = nova_session:delete(Req, <<"remove">>),
    sync(),
    ?assertEqual({ok, <<"yes">>}, nova_session:get(Req, <<"keep">>)),
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"remove">>)).

cookie_fallback() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_cookie(SId),
    ok = nova_session:set(Req, <<"via_cookie">>, <<"works">>),
    sync(),
    ?assertEqual({ok, <<"works">>}, nova_session:get(Req, <<"via_cookie">>)).

no_session_returns_error() ->
    Req = #{},
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"key">>)),
    ?assertEqual({error, session_id_not_set}, nova_session:set(Req, <<"k">>, <<"v">>)).

no_session_delete_returns_ok() ->
    Req = #{},
    ?assertEqual({ok, Req}, nova_session:delete(Req)),
    ?assertEqual({ok, Req}, nova_session:delete(Req, <<"key">>)).

sessions_disabled_throws() ->
    application:set_env(nova, use_sessions, false),
    Req = req_with_session(<<"id">>),
    ?assertThrow({nova_session, unsupported_session_used}, nova_session:get(Req, <<"k">>)),
    ?assertThrow({nova_session, unsupported_session_used}, nova_session:set(Req, <<"k">>, <<"v">>)),
    ?assertThrow({nova_session, unsupported_session_used}, nova_session:delete(Req)),
    ?assertThrow({nova_session, unsupported_session_used}, nova_session:delete(Req, <<"k">>)),
    application:set_env(nova, use_sessions, true).

delete_sets_expired_cookie() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"key">>, <<"val">>),
    sync(),
    {ok, Req1} = nova_session:delete(Req),
    ?assertMatch(#{resp_cookie := #{name := <<"session_id">>, opts := #{max_age := 0}}}, Req1).

delete_key_preserves_other_keys() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"a">>, <<"1">>),
    ok = nova_session:set(Req, <<"b">>, <<"2">>),
    ok = nova_session:set(Req, <<"c">>, <<"3">>),
    sync(),
    {ok, _} = nova_session:delete(Req, <<"b">>),
    sync(),
    ?assertEqual({ok, <<"1">>}, nova_session:get(Req, <<"a">>)),
    ?assertEqual({error, not_found}, nova_session:get(Req, <<"b">>)),
    ?assertEqual({ok, <<"3">>}, nova_session:get(Req, <<"c">>)).

set_overwrites_existing_key() ->
    {ok, SId} = nova_session:generate_session_id(),
    Req = req_with_session(SId),
    ok = nova_session:set(Req, <<"key">>, <<"old">>),
    sync(),
    ok = nova_session:set(Req, <<"key">>, <<"new">>),
    sync(),
    ?assertEqual({ok, <<"new">>}, nova_session:get(Req, <<"key">>)).

generate_session_id_format() ->
    {ok, Id} = nova_session:generate_session_id(),
    ?assert(is_binary(Id)),
    Decoded = base64:decode(Id),
    ?assertEqual(32, byte_size(Decoded)),
    {ok, Id2} = nova_session:generate_session_id(),
    ?assertNotEqual(Id, Id2).

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.