Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

dagolden's collected fixes for sessions #150

Closed
wants to merge 23 commits into from

4 participants

@dagolden

This pull request fixes almost all of the outstanding issues with sessions that I found.

I may have a few more tests drifting in later, but I think this should be merged so others can use it for further refinements and so additional D2-ready session factories can be written against it.

dagolden added some commits
@dagolden dagolden Delete session data key when value set to undef
Without a way to delete session data, sessions can only grow until
expired.

This patch deletes the key from the session data hash if the value is
set to undef.  Querying a non-existent key returns undef, so deleting a
key has the same effective result as setting it to undef with the added
benefit of shrinking the session.

Documentation of these behaviors and a test are included.
37d62ef
@dagolden dagolden rename Dancer::Session::* to Dancer::SessionFactory::* 279d876
@dagolden dagolden Move session_dir default to YAML session class
Session directories are a concept specific to file-based sessions.
It doesn't belong in the Core::Role::Config class.
b56f9ba
@dagolden dagolden Revise SessionFactory implentation API
Instead of handing off entire session objects to _flush, only the
id and data hashrefs are handed off.  This ensures that session object
attributes are not serialized.

Likewise, _retrieve now needs only to return a data hashref, which
SessionFactory will instantiate into a new object with *current*
session_config settings.  This ensures things like expiration time
are reset correctly.
9ed793e
@dagolden dagolden Simple SessionFactory should delete during destory af69b9a
@dagolden dagolden allow session objects to destroy themselves 1b3a2ff
@dagolden dagolden Move cookie_name into SessionFactory
This eliminates the hard-coded 'dancer.session' cookie name
in Dancer::Core::Context.
2fe527d
@dagolden dagolden Set session cookie after request, not before
This ensures that cookie based sessions have a chance to flush
data into the session cookie value before the cookie is set.

This also attempts to only set a session cookie if one existed
or if a session has been referenced.
2eb5163
@bigpresh
Owner

All looks good & sane to me - would like another core dev to approve too though.

Note: Travis CI thinks the build failed, but it only failed on 5.12, the other versions passed all tests; the failure appears to be spurious, the error was:

Executing your  (perlbrew use 5.12) took longer than 3 minutes and was terminated.

... so yeah, ignore the fact Travis reports test failures.

@bigpresh
Owner

I poked Travis to re-run the tests - all pass now.

@sukria sukria commented on the diff
lib/Dancer/Core/App.pm
@@ -149,8 +149,13 @@ sub session {
# read if a key is provided
return $session->read($key) if @_ == 2;
- # write to the session
- $session->write($key => $value);
+ # write to the session or delete if value is undef
@sukria Owner
sukria added a note

Not sure about that, in some cases, maybe the user wants to set undef to a value in the session, no? Not sure...

@dagolden
dagolden added a note

Because reading from a non-existent value still returns undef, functionally it won't matter as long as they are using the Session object API. It only matters if people are checking the underlying data hash ( exists(session->data->{$key} ) and that's sort of an encapsulation violation anyway.

I'm don't feel terribly strongly, about it. session->delete($key) may be sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@sukria
Owner

Thanks for all the work, it needs to be fully tested though, I have a real-life app based on Dancer 2, which uses sessions, at work, I'll test it against this PR before the merge.

Also, we should document that in D2, sessiion engines are named under the SessionFactory namespace now.

dagolden added some commits
@dagolden dagolden improve has_session predicate
Replaces the standard 'has_session' predicate with one that is only true
if a session engine has been defined and either a session has been
retrieved or created or a session cookie was retrieved in the request.

If this is true, calling $context->session returns a session that
previously existed.  If false, calling $context->session creates a new
session (and subsequent has_session() calls are true) or dies if a
session engine is not defined.
745fb39
@dagolden dagolden Remove creation_time attribute from core Session class
The 'creation_time' attribute is unused and the semantics are not
defined.

Expiration time in the Session class is cookie expiration time,
which may or may not be the same as session expiration, since cookie
expiration is under the control of the user.

If creation_time were a required field that session factories were
mandated to create and preserve on session retrieval, then it could be
used to help establish an independent session duration limit, but this
is not currently the case.

If session duration limits are desired, it is probably better for
applications to develop their own logic using data stored within
the session.

Depending on the nature of the session backend, session factories
could independently track creation time for use in offline
session expiration and disk/memory recovery, but this does not
require an attribute within the core Session class.
1374a35
@dagolden dagolden Move session cookie management to SessionFactory
This commit moves cookie generation from Dancer::Core::Session
to Dancer::Core::Role::SessionFactory and moves universal
session cookie configuration parameters as well.

The rationale is that SessionFactories should manage implementation
details, such as knowing how cookies are set and retrieved, and should
have configuration that applies to all sessions (such as 'is_secure').

Dancer::Core::Session objects can then purely represent the specific
details of a *specific* session: the ID, the data, and the expiration.
cad9b39
@dagolden dagolden cleanup Dancer::Core::Session
Groups attributes together, fixes documentation and removes trailing
spaces.
4db982e
@dagolden dagolden Fix documentation of SessionFactory
Changes POD headers to head2 for consistency.

Clarifies return values required by C<_sessions>.
155c7a5
@dagolden

I have some better ideas about session destruction and how to avoid proliferating empty sessions. I'll try to get those worked up and committed tomorrow or Tuesday.

@celogeek
Owner

Some tests are missing :

for SessionFactory :

cookie_duration. did you test when a session can expire or not ?

what happen if you can't _flush a session (issue with the disk or anythink), did you test that ? is it good to crash the application in that case ?

did the destroy do properly his job ? did you test it ?

For Dancer::Core::Session :

what happen if you do session->read(undef) ? or session->read ?

same of session->write (without key), is it possible to happen ?

same for delete :)

did you test the destroy ? with the expire ? does it work ?

for Dancer::SessionFactory::Simple :

I see a global vars where you save your data. Did the goal of Dancer2 was to be scoped by module name ? or at least for sharing information, may be the module can call the session of another module ?

did you test the method _retrieve, _sessions, _destroy ?

@sukria
Owner

After live-testing that PR:

If the sessions are removed (using the YAML backend) but the client has a cookie:

Exception caught in 'core.app.before_request' filter: Hook error: Can't use an undefined value as a HASH reference at ..../Dancer2/lib/Dancer/Core/Session.pm line 100.

Also, when a session is created, an artifact appears in the session dir:

ls sessions/
a371352d629c232c6938c54ca516a5fdf7742e70.yml  Dancer::Core::Session=HASH(0x56a38d8).yml

These bugs don't exist in master.

@sukria
Owner

I tend to agree with you, but maybe it's better to discuss that before a removal in the code... no? :)

It's easy to put stuff back. I wouldn't have removed it if there was anything in lib/ that used it. :-)

@dagolden

@sukria, after I revise session destruction, I think those problems may go away. Can you be more specific what you mean about "removed using the YAML backend" and what your before_request hook is doing so I can try to get a test fro it?

@celogeek:

  • most of the lack of test coverage pre-dates my work; I plan to gradually improve tests once my work resolves in my own mind the correct behavior of things. :-)

  • _flush exception crashing the app was preexisting. It seems wise. What else do you suggest? I could imagine a config option that switches the behavior from "die" to "destroy session", but that seems likely to lead to strange errors that aren't noticed for a while. (E.g. user suddenly logged out for no reason.)

  • Dancer::SessionFactory::Simple was merely renamed. It already used a global, which is fine because sessions are global. (E.g. SessionFactory::YAML uses disk as its global.).

@dagolden

@sukria, I found the .yml artifact, but have a question. Why does SessionFactory::create() call "_flush" and not "flush"? Why do you want to by pass flush hooks if you're actually flushing?

Separately, why not defer flushing new sessions until the end of the request as usual?

dagolden added some commits
@dagolden dagolden flush sessions properly during create() 46b3997
@dagolden dagolden ensure retrieved sessions have correct data hash a277fd4
@dagolden dagolden Moved session destruction to context
Sessions are now destroyed via the context object.  This is necessary
because sessions are managed in multiple places and need synchronized
behavior.

In addition to destroying the session via the factory, when a session is
destroyed we need to expire the existing session cookie and send it
back in the response, but only if another session does not replace it.

Likewise, once a session is destroyed, we must ignore the existing
request session cookie and not attempt to retrieve it again.

As a side benefit of this change, Dancer::Core::Session objects no
longer need a link back to their factory, which makes for cleaner
design.

This commit includes more extensive session lifecycle tests.
c2393b4
@dagolden

@sukria, please review the new destruction commit. I think it's much saner.

@sukria
Owner
@sukria
Owner

Hey @dagolden, I tested your last changes and it works pretty fine now. I have still a couple of remarks regarding the code, but mostly cosmectic things, I'm going to comment on the revelant parts.

After that, I think we'll be able to merge :)

Thanks a lot for the impressive work on this subject, sessions handling is going to kick ass.

lib/Dancer/Core/App.pm
((33 lines not shown))
return if ! defined $self->context;
- $engine->flush(session => $self->context->session);
+
+ # if a session has been instantiated or we already had a
+ # session, first flush the session so cookie-based sessions can
+ # update the session ID if needed, then set the session cookie
+ # in the response
+
+ my $session;
+ if ( $self->context->has_session ) {
+ $session = $self->context->session;
+ $engine->flush(session => $session);
+ }
+ elsif ( $self->context->_has_destroyed_session ) {
+ $session = $self->context->_destroyed_session;
@sukria Owner
sukria added a note

I'm a bit puzzled by the fact that we call a private method from the outside: if we know about _destroyed_session then it should be public, no? This appears elsewhere, I won't comment everywhere but the same remark applies.

@dagolden
dagolden added a note

Oops.

Originally, I didn't have this here because I set the cookie during destroy_session. But tests caught that that sets two session cookies, because they are pushed onto headers instead of replacing. So this code migrated out here and I never made the method public.

I don't really like having to cache a destroyed session and having it public, but it's not the worst thing in the world.

An alternative would be to add a "session_cookie" attribute to Dancer::Core::Response and only push that to the headers in Dancer::Core::Reponse::to_psgi. Then destroy_session in Context could set that on destroy, and the hook in App would overwrite it if a new session exists and the extra _has_destroyed_session clause could go away.

What do you think?

@sukria Owner
sukria added a note

I'm not sure we want to alter Dancer::Core::Response with session-specific data (here the "session_cookie" attribute). I think that the Response class should not know about a "session".

It's fine for me if in your current branch, the private methods are renamed with public names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Dancer/Core/Context.pm
@@ -114,22 +115,80 @@ sub _build_session {
if ! defined $engine;
# find the session cookie if any
- my $session_id;
- my $session_cookie = $self->cookie('dancer.session');
- if (defined $session_cookie) {
- $session_id = $session_cookie->value;
- }
-
- # if we have a session cookie, try to retrieve the session
- if (defined $session_id) {
- eval { $session = $engine->retrieve(id => $session_id) };
- croak "Fail to retreive session: $@"
- if $@ && $@ !~ /Unable to retrieve session/;
+ unless ( $self->_destroyed_session ) {
@sukria Owner
sukria added a note

I really don't like unless blocks, I use unless exclusively for postfixed conditions, because with a block, the temptation of introducing an "else" later on is dangerous... What about unless (condition) { } else { } ?

I would really prefer to see if (! $self->destroyed_session) here (same remark for the private method).

I know these can be subjective remarks, but if we can share common coding guidelines in the core, it's better ;)

BTW @celogeek : do we have a PerlCritic policy implemented with dzil? This kind of stuff should be triggered by our policy.

@dagolden
dagolden added a note

Will fix. And I'll see if I can get dzil tests running to check against policy stuff.

@dagolden
dagolden added a note

FWIW, it's not the only place in the code that qr/unless (/ appears.

@sukria Owner
sukria added a note
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@sukria
Owner

Merged! Thanks a lot for the awesome work David!

@sukria sukria closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 4, 2013
  1. @dagolden

    Delete session data key when value set to undef

    dagolden authored
    Without a way to delete session data, sessions can only grow until
    expired.
    
    This patch deletes the key from the session data hash if the value is
    set to undef.  Querying a non-existent key returns undef, so deleting a
    key has the same effective result as setting it to undef with the added
    benefit of shrinking the session.
    
    Documentation of these behaviors and a test are included.
  2. @dagolden
  3. @dagolden

    Move session_dir default to YAML session class

    dagolden authored
    Session directories are a concept specific to file-based sessions.
    It doesn't belong in the Core::Role::Config class.
  4. @dagolden

    Revise SessionFactory implentation API

    dagolden authored
    Instead of handing off entire session objects to _flush, only the
    id and data hashrefs are handed off.  This ensures that session object
    attributes are not serialized.
    
    Likewise, _retrieve now needs only to return a data hashref, which
    SessionFactory will instantiate into a new object with *current*
    session_config settings.  This ensures things like expiration time
    are reset correctly.
  5. @dagolden
Commits on Jan 5, 2013
  1. @dagolden
  2. @dagolden

    Move cookie_name into SessionFactory

    dagolden authored
    This eliminates the hard-coded 'dancer.session' cookie name
    in Dancer::Core::Context.
  3. @dagolden

    Set session cookie after request, not before

    dagolden authored
    This ensures that cookie based sessions have a chance to flush
    data into the session cookie value before the cookie is set.
    
    This also attempts to only set a session cookie if one existed
    or if a session has been referenced.
Commits on Jan 6, 2013
  1. @dagolden
  2. @dagolden

    improve has_session predicate

    dagolden authored
    Replaces the standard 'has_session' predicate with one that is only true
    if a session engine has been defined and either a session has been
    retrieved or created or a session cookie was retrieved in the request.
    
    If this is true, calling $context->session returns a session that
    previously existed.  If false, calling $context->session creates a new
    session (and subsequent has_session() calls are true) or dies if a
    session engine is not defined.
Commits on Jan 7, 2013
  1. @dagolden

    Remove creation_time attribute from core Session class

    dagolden authored
    The 'creation_time' attribute is unused and the semantics are not
    defined.
    
    Expiration time in the Session class is cookie expiration time,
    which may or may not be the same as session expiration, since cookie
    expiration is under the control of the user.
    
    If creation_time were a required field that session factories were
    mandated to create and preserve on session retrieval, then it could be
    used to help establish an independent session duration limit, but this
    is not currently the case.
    
    If session duration limits are desired, it is probably better for
    applications to develop their own logic using data stored within
    the session.
    
    Depending on the nature of the session backend, session factories
    could independently track creation time for use in offline
    session expiration and disk/memory recovery, but this does not
    require an attribute within the core Session class.
  2. @dagolden

    Move session cookie management to SessionFactory

    dagolden authored
    This commit moves cookie generation from Dancer::Core::Session
    to Dancer::Core::Role::SessionFactory and moves universal
    session cookie configuration parameters as well.
    
    The rationale is that SessionFactories should manage implementation
    details, such as knowing how cookies are set and retrieved, and should
    have configuration that applies to all sessions (such as 'is_secure').
    
    Dancer::Core::Session objects can then purely represent the specific
    details of a *specific* session: the ID, the data, and the expiration.
  3. @dagolden

    cleanup Dancer::Core::Session

    dagolden authored
    Groups attributes together, fixes documentation and removes trailing
    spaces.
  4. @dagolden

    Fix documentation of SessionFactory

    dagolden authored
    Changes POD headers to head2 for consistency.
    
    Clarifies return values required by C<_sessions>.
  5. @dagolden
  6. @dagolden
Commits on Jan 8, 2013
  1. @dagolden

    Moved session destruction to context

    dagolden authored
    Sessions are now destroyed via the context object.  This is necessary
    because sessions are managed in multiple places and need synchronized
    behavior.
    
    In addition to destroying the session via the factory, when a session is
    destroyed we need to expire the existing session cookie and send it
    back in the response, but only if another session does not replace it.
    
    Likewise, once a session is destroyed, we must ignore the existing
    request session cookie and not attempt to retrieve it again.
    
    As a side benefit of this change, Dancer::Core::Session objects no
    longer need a link back to their factory, which makes for cleaner
    design.
    
    This commit includes more extensive session lifecycle tests.
  2. @dagolden
  3. @dagolden

    add tests for SessionFactory config

    dagolden authored
    Test that session cookies can be modified by engine config settings.
  4. @dagolden
  5. @dagolden
  6. @dagolden

    Tidy session-related changes

    dagolden authored
    While a .perltidyrc exists, most source files were not tidy.  I have
    attempted to tidy session-related code I was working on.
  7. @dagolden
This page is out of date. Refresh to see the latest.
View
10 lib/Dancer/Cookbook.pod
@@ -359,19 +359,21 @@ Or, alternatively,
For disc-based session back ends like L<Dancer::Session::YAML>,
L<Dancer::Session::Storable> etc, session files are written to the session dir
-specified by the C<session_dir> setting, which defaults to C<appdir/sessions> if
+specified by the C<session_dir> setting, which defaults to C<./sessions> if
not specifically set.
If you need to control where session files are created, you can do so quickly
and easily within your config file, for example:
- session_dir: /tmp/dancer-sessions
+ session: YAML
+ engines:
+ session:
+ YAML:
+ session_dir: /tmp/dancer-sessions
If the directory you specify does not exist, Dancer will attempt to create it
for you.
-
-
=head3 Destroying a session
When you're done with your session, you can destroy it:
View
55 lib/Dancer/Core/App.pm
@@ -141,7 +141,7 @@ sub session {
my $session = $self->context->session;
croak "No session available, a session engine needs to be set"
- if ! defined $session;
+ if !defined $session;
# return the session object if no key
return $session if @_ == 1;
@@ -149,8 +149,13 @@ sub session {
# read if a key is provided
return $session->read($key) if @_ == 2;
- # write to the session
- $session->write($key => $value);
+ # write to the session or delete if value is undef
@sukria Owner
sukria added a note

Not sure about that, in some cases, maybe the user wants to set undef to a value in the session, no? Not sure...

@dagolden
dagolden added a note

Because reading from a non-existent value still returns undef, functionally it won't matter as long as they are using the Session object API. It only matters if people are checking the underlying data hash ( exists(session->data->{$key} ) and that's sort of an encapsulation violation anyway.

I'm don't feel terribly strongly, about it. session->delete($key) may be sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if (defined $value) {
+ $session->write($key => $value);
+ }
+ else {
+ $session->delete($key);
+ }
}
sub template {
@@ -347,33 +352,39 @@ sub BUILD {
sub _init_hooks {
my ($self) = @_;
- # Hook to add the session cookie in the headers, if a session is defined
- $self->add_hook(Dancer::Core::Hook->new(
- name => 'core.app.before_request',
- code => sub {
- my $context = shift;
-
- # make sure an engine is defined, if not, nothing to do
- my $engine = $self->setting('session');
- return if ! defined $engine;
-
- # push the session in the headers
- $context->response->push_header('Set-Cookie',
- $context->session->cookie->to_header);
- }
- ));
-
# Hook to flush the session at the end of the request, this way, we're sure we
# flush only once per request
$self->add_hook(
Dancer::Core::Hook->new(
name => 'core.app.after_request',
code => sub {
+ my $response = shift;
+
# make sure an engine is defined, if not, nothing to do
my $engine = $self->setting('session');
- return if ! defined $engine;
- return if ! defined $self->context;
- $engine->flush(session => $self->context->session);
+ return if !defined $engine;
+
+ # make sure we have a context to examine
+ return if !defined $self->context;
+
+ # if a session has been instantiated or we already had a
+ # session, first flush the session so cookie-based sessions can
+ # update the session ID if needed, then set the session cookie
+ # in the response
+
+ my $session;
+ if ($self->context->has_session) {
+ $session = $self->context->session;
+ $engine->flush(session => $session);
+ }
+ elsif ($self->context->has_destroyed_session) {
+ $session = $self->context->destroyed_session;
+ }
+
+ if ($session) {
+ $response->push_header('Set-Cookie',
+ $engine->cookie(session => $session)->to_header);
+ }
},
)
);
View
88 lib/Dancer/Core/Context.pm
@@ -102,6 +102,7 @@ has session => (
isa => Session,
lazy => 1,
builder => '_build_session',
+ clearer => 1,
);
sub _build_session {
@@ -111,25 +112,88 @@ sub _build_session {
# Find the session engine
my $engine = $self->app->setting('session');
croak "No session engine defined, cannot use session."
- if ! defined $engine;
+ if !defined $engine;
# find the session cookie if any
- my $session_id;
- my $session_cookie = $self->cookie('dancer.session');
- if (defined $session_cookie) {
- $session_id = $session_cookie->value;
- }
-
- # if we have a session cookie, try to retrieve the session
- if (defined $session_id) {
- eval { $session = $engine->retrieve(id => $session_id) };
- croak "Fail to retreive session: $@"
- if $@ && $@ !~ /Unable to retrieve session/;
+ if (!$self->destroyed_session) {
+ my $session_id;
+ my $session_cookie = $self->cookie($engine->cookie_name);
+ if (defined $session_cookie) {
+ $session_id = $session_cookie->value;
+ }
+
+ # if we have a session cookie, try to retrieve the session
+ if (defined $session_id) {
+ eval { $session = $engine->retrieve(id => $session_id) };
+ croak "Fail to retreive session: $@"
+ if $@ && $@ !~ /Unable to retrieve session/;
+ }
}
# create the session if none retrieved
return $session ||= $engine->create();
}
+=method has_session
+
+Returns true if session engine has been defined and if either a session object
+has been instantiated in the context or if a session cookie was found and not
+subsequently invalidated.
+
+=cut
+
+sub has_session {
+ my ($self) = @_;
+
+ my $engine = $self->app->setting('session')
+ or return;
+
+ return $self->{session}
+ || ($self->cookie($engine->cookie_name) && !$self->destroyed_session);
+}
+
+=attr destroyed_session
+
+We cache a destroyed session here; once this is set we must not attempt to
+retrieve the session from the cookie in the request. If no new session is
+created, this is set (with expiration) as a cookie to force the browser to
+expire the cookie.
+
+=cut
+
+has destroyed_session => (
+ is => 'rw',
+ isa => InstanceOf ['Dancer::Core::Session'],
+ predicate => 1,
+);
+
+=method destroy_session
+
+Destroys the current session and ensures any subsquent session is created
+from scratch and not from the request session cookie
+
+=cut
+
+sub destroy_session {
+ my ($self) = @_;
+
+ # Find the session engine
+ my $engine = $self->app->setting('session');
+ croak "No session engine defined, cannot use session."
+ if !defined $engine;
+
+ # Expire session, set the expired cookie and destroy the session
+ # Setting the cookie ensures client gets an expired cookie unless
+ # a new session is created and supercedes it
+ my $session = $self->session;
+ $session->expires(-86400); # yesterday
+ $engine->destroy(id => $session->id);
+
+ # Clear session in context and invalidate session cookie in request
+ $self->destroyed_session($session);
+ $self->clear_session;
+
+ return;
+}
1;
View
5 lib/Dancer/Core/Role/Config.pm
@@ -231,11 +231,8 @@ my $_setters = {
my $engine_options =
$self->_get_config_for_engine(session => $value, $config);
- $engine_options->{session_dir}
- ||= File::Spec->catdir($self->config_location, 'sessions');
-
return Dancer::Factory::Engine->create(
- session => $value,
+ session_factory => $value,
%{$engine_options},
postponed_hooks => $self->get_postponed_hooks,
);
View
1  lib/Dancer/Core/Role/Hookable.pm
@@ -51,6 +51,7 @@ sub _add_postponed_hooks {
my $caller = ref($self);
my ( $dancer, $h_type, $h_name, @rest ) = map { lc } split /::/, $caller;
$h_name = $rest[0] if $h_name eq 'Role';
+ $h_type = 'session' if $h_type eq 'sessionfactory';
if ( $h_type =~ /(template|logger|serializer|session)/ ) {
$h_name = $h_type;
$h_type = 'engine';
View
206 lib/Dancer/Core/Role/SessionFactory.pm
@@ -20,37 +20,105 @@ use Moo::Role;
with 'Dancer::Core::Role::Engine';
sub supported_hooks {
- qw/
- engine.session.before_retrieve
- engine.session.after_retrieve
+ qw/
+ engine.session.before_retrieve
+ engine.session.after_retrieve
- engine.session.before_create
- engine.session.after_create
+ engine.session.before_create
+ engine.session.after_create
- engine.session.before_destroy
- engine.session.after_destroy
+ engine.session.before_destroy
+ engine.session.after_destroy
- engine.session.before_flush
- engine.session.after_flush
- /
+ engine.session.before_flush
+ engine.session.after_flush
+ /;
}
-sub _build_type {'Session'}
+sub _build_type {
+ 'SessionFactory'
+} # XXX vs 'Session'? Unused, so I can't tell -- xdg
-=attr session_config
+=attr cookie_name
-A HashRef that contains all config options that should be passed to the
-constructor of L<Dancer::Core::Session>.
+The name of the cookie to create for storing the session key
-By default this Hash is empty so all default values will be taken as described
-in the L<Dancer::Core::Session> class.
+Defaults to C<dancer.session>
=cut
-has session_config => (
- is => 'ro',
- isa => HashRef,
- default => sub { {} },
+has cookie_name => (
+ is => 'ro',
+ isa => Str,
+ default => sub {'dancer.session'},
+);
+
+=attr cookie_domain
+
+The domain of the cookie to create for storing the session key.
+Defaults to the empty string and is unused as a result.
+
+=cut
+
+has cookie_domain => (
+ is => 'ro',
+ isa => Str,
+ predicate => 1,
+);
+
+=attr cookie_path
+
+The path of the cookie to create for storing the session key.
+Defaults to "/".
+
+=cut
+
+has cookie_path => (
+ is => 'ro',
+ isa => Str,
+ default => sub {"/"},
+);
+
+=attr cookie_duration
+
+Default duration before session cookie expiration. If set, the
+L<Dancer::Core::Session> C<expires> attribute will be set to the current time
+plus this duration.
+
+=cut
+
+has cookie_duration => (
+ is => 'ro',
+ isa => Num,
+ predicate => 1,
+);
+
+=attr is_secure
+
+Boolean flag to tell if the session cookie is secure or not.
+
+Default is false.
+
+=cut
+
+has is_secure => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {0},
+);
+
+=attr is_http_only
+
+Boolean flag to tell if the session cookie is http only.
+
+Default is true.
+
+=cut
+
+has is_http_only => (
+ is => 'rw',
+ isa => Bool,
+ default => sub {1},
);
=head1 INTERFACE
@@ -75,14 +143,18 @@ This method does not need to be implemented in the class.
sub create {
my ($self) = @_;
- my $session = Dancer::Core::Session->new(
- %{$self->session_config},
- id => $self->generate_id,
- );
+
+ my %args = (id => $self->generate_id,);
+
+ $args{expires} = $self->cookie_duration
+ if $self->has_cookie_duration;
+
+ my $session = Dancer::Core::Session->new(%args);
+
$self->execute_hook('engine.session.before_create', $session);
- eval { $self->_flush($session) };
- croak "Unable to create a new session: $@"
+ eval { $self->_flush($session->id, $session->data) };
+ croak "Unable to create a new session: $@"
if $@;
$self->execute_hook('engine.session.after_create', $session);
@@ -107,17 +179,16 @@ alternative method for session ID generation is desired.
sub generate_id {
my ($self) = @_;
- my $seed = rand(1_000_000_000) # a random number
- . __FILE__ # the absolute path as a secret key
- . $COUNTER++ # impossible to have two consecutive dups
- . time() # impossible to have dups between seconds
- . $$ # the process ID as another private constant
- . "$self" # the instance's memory address for more entropy
- . join('',
- shuffle('a'..'z',
- 'A'..'Z',
- 0 .. 9)) # a shuffled list of 62 chars, another random component
- ;
+ my $seed = rand(1_000_000_000) # a random number
+ . __FILE__ # the absolute path as a secret key
+ . $COUNTER++ # impossible to have two consecutive dups
+ . time() # impossible to have dups between seconds
+ . $$ # the process ID as another private constant
+ . "$self" # the instance's memory address for more entropy
+ . join('',
+ shuffle('a' .. 'z', 'A' .. 'Z', 0 .. 9)
+ ) # a shuffled list of 62 chars, another random component
+ ;
return sha1_hex($seed);
}
@@ -131,7 +202,8 @@ found, triggers an exception.
my $session = MySessionFactory->retrieve(id => $id);
-The method C<_retrieve> must be implemented.
+The method C<_retrieve> must be implemented. It must take C<$id> as a single
+argument and must return a hash reference of session data.
=cut
@@ -139,15 +211,24 @@ requires '_retrieve';
sub retrieve {
my ($self, %params) = @_;
- my $session;
my $id = $params{id};
$self->execute_hook('engine.session.before_retrieve', $id);
- eval { $session = $self->_retrieve($id) };
+ my $data = eval { $self->_retrieve($id) };
croak "Unable to retrieve session with id '$id'"
if $@;
+ my %args = (id => $id,);
+
+ $args{data} = $data
+ if $data and ref $data eq 'HASH';
+
+ $args{expires} = $self->cookie_duration
+ if $self->has_cookie_duration;
+
+ my $session = Dancer::Core::Session->new(%args);
+
$self->execute_hook('engine.session.after_retrieve', $session);
return $session;
}
@@ -159,7 +240,8 @@ destroyed session if succeeded, triggers an exception otherwise.
MySessionFactory->destroy(id => $id);
-The C<_destroy> method must be implemented.
+The C<_destroy> method must be implemented. It must take C<$id> as a single
+argumenet and destroy the underlying data.
=cut
@@ -187,7 +269,8 @@ An exception is triggered if the session is unable to be updated in the backend.
MySessionFactory->flush(session => $session);
-The C<_flush> method must be implemented.
+The C<_flush> method must be implemented. It must take two arguments: the C<$id>
+and a hash reference of session data.
=cut
@@ -198,7 +281,7 @@ sub flush {
my $session = $params{session};
$self->execute_hook('engine.session.before_flush', $session);
- eval { $self->_flush($session) };
+ eval { $self->_flush($session->id, $session->data) };
croak "Unable to flush session: $@"
if $@;
@@ -206,13 +289,46 @@ sub flush {
return $session->id;
}
+=head2 cookie
+
+Coerce a session object into a L<Dancer::Core::Cookie> object.
+
+ MySessionFactory->cookie(session => $session);
+
+=cut
+
+sub cookie {
+ my ($self, %params) = @_;
+ my $session = $params{session};
+ croak "cookie() requires a valid 'session' parameter"
+ unless ref($session) && $session->isa("Dancer::Core::Session");
+
+ my %cookie = (
+ value => $session->id,
+ name => $self->cookie_name,
+ path => $self->cookie_path,
+ secure => $self->is_secure,
+ http_only => $self->is_http_only,
+ );
+
+ $cookie{domain} = $self->cookie_domain
+ if $self->has_cookie_domain;
+
+ if (my $expires = $session->expires) {
+ $cookie{expires} = $expires;
+ }
+
+ return Dancer::Core::Cookie->new(%cookie);
+}
+
-=method sessions
+=head2 sessions
Return a list of all session IDs stored in the backend.
Useful to create cleaning scripts, in conjunction with session's creation time.
-Required method : C<_sessions>
+The C<_sessions> method must be implemented. It must return an array reference
+of session IDs (or an empty array reference).
=cut
View
2  lib/Dancer/Core/Role/Template.pm
@@ -143,7 +143,7 @@ sub _prepare_tokens_options {
$tokens->{vars} = $self->context->buffer;
$tokens->{session} = $self->context->session->data
- if defined $self->context->app->setting('session');
+ if $self->context->has_session;
}
return $tokens;
View
138 lib/Dancer/Core/Session.pm
@@ -1,10 +1,11 @@
package Dancer::Core::Session;
+
#ABSTRACT: class to represent any session object
=head1 DESCRIPTION
-A session object encapsulates anything related to a specific session: it's ID,
-its data, creation timestampe...
+A session object encapsulates anything related to a specific session: its ID,
+its data, and its expiration.
It is completely agnostic of how it will be stored, this is the role of
a factory that consumes L<Dancer::Core::Role::SessionFactory> to know about that.
@@ -21,7 +22,6 @@ use warnings;
use Moo;
use Dancer::Core::Types;
-
=attr id
The identifier of the session object. Required. By default,
@@ -31,69 +31,27 @@ guaranteed-unique string.
=cut
has id => (
- is => 'rw',
- isa => Str,
- required => 1,
+ is => 'rw',
+ isa => Str,
+ required => 1,
);
-=method read
-
-Reader on the session data
-
- my $value = $session->read('something');
-
-=cut
-
-sub read {
- my ($self, $key) = @_;
- return $self->data->{$key};
-}
-
-
-=method write
-
-Writer on the session data
-
-=cut
-
-sub write {
- my ($self, $key, $value) = @_;
- $self->data->{$key} = $value;
-}
-
-=attr is_secure
-
-Boolean flag to tell if the session cookie is secure or not.
-
-Default is false.
-
-=cut
-
-has is_secure => (
- is => 'rw',
- isa => Bool,
- default => sub { 0 },
-);
-
-=attr is_http_only
-
-Boolean flag to tell if the session cookie is http only.
+=attr data
-Default is true.
+Contains the data of the session (Hash).
=cut
-has is_http_only => (
- is => 'rw',
- isa => Bool,
- default => sub { 1 },
+has data => (
+ is => 'rw',
+ lazy => 1,
+ default => sub { {} },
);
-
=attr expires
Number of seconds for the expiry of the session cookie. Don't add the current
-timestamp to it, will be done automatically.
+timestamp to it, will be done automatically.
Default is no expiry (session cookie will leave for the whole browser's
session).
@@ -105,74 +63,58 @@ For a lifetime of one hour:
=cut
has expires => (
- is => 'rw',
- isa => Str,
+ is => 'rw',
+ isa => Str,
coerce => sub {
my $value = shift;
$value += time;
},
);
+=method read
-=attr data
-
-Contains the data of the session (Hash).
+Reader on the session data
-=cut
+ my $value = $session->read('something');
-has data => (
- is => 'rw',
- lazy => 1,
- default => sub { {} },
-);
+Returns C<undef> if the key does not exist in the session.
-=attr creation_time
+=cut
-A timestamp of the moment when the session was created.
+sub read {
+ my ($self, $key) = @_;
+ return $self->data->{$key};
+}
-=cut
-has creation_time => (
- is => 'ro',
- default => sub { time() },
-);
+=method write
-=attr cookie_name
+Writer on the session data
-The name of the cookie to create for storing the session key
+ $session->write('something', $value);
-Defaults to C<dancer.session>
+Returns C<$value>.
=cut
-has cookie_name => (
- is => 'ro',
- isa => Str,
- default => sub { 'dancer.session' },
-);
-
-=method cookie
+sub write {
+ my ($self, $key, $value) = @_;
+ $self->data->{$key} = $value;
+}
-Coerce the session object into a L<Dancer::Core::Cookie> object.
+=method delete
-=cut
+Deletes a key from session data
-sub cookie {
- my ($self) = @_;
+ $session->delete('something');
- my %cookie = (
- name => $self->cookie_name,
- value => $self->id,
- secure => $self->is_secure,
- http_only => $self->is_http_only,
- );
+Returns the value deleted from the session.
- if (my $expires = $self->expires) {
- $cookie{expires} = $expires;
- }
+=cut
- return Dancer::Core::Cookie->new(%cookie);
+sub delete {
+ my ($self, $key, $value) = @_;
+ delete $self->data->{$key};
}
-
1;
View
12 lib/Dancer/Session/Simple.pm → lib/Dancer/SessionFactory/Simple.pm
@@ -1,6 +1,6 @@
# ABSTRACT: in-memory session backend for Dancer
-package Dancer::Session::Simple;
+package Dancer::SessionFactory::Simple;
use Moo;
use Dancer::Core::Types;
use Carp;
@@ -31,7 +31,7 @@ engine in a Dancer application.
sub _sessions {
my ($self) = @_;
- return [ keys %{ $SESSIONS } ];
+ return [keys %{$SESSIONS}];
}
sub _retrieve {
@@ -39,19 +39,19 @@ sub _retrieve {
my $s = $SESSIONS->{$id};
croak "Invalid session ID: $id"
- if ! defined $s;
+ if !defined $s;
return $s;
}
sub _destroy {
my ($class, $id) = @_;
- undef $SESSIONS->{$id};
+ delete $SESSIONS->{$id};
}
sub _flush {
- my ($class, $session) = @_;
- $SESSIONS->{$session->id} = $session;
+ my ($class, $id, $data) = @_;
+ $SESSIONS->{$id} = $data;
}
1;
View
41 lib/Dancer/Session/YAML.pm → lib/Dancer/SessionFactory/YAML.pm
@@ -1,4 +1,5 @@
-package Dancer::Session::YAML;
+package Dancer::SessionFactory::YAML;
+
# ABSTRACT: YAML-file-based session backend for Dancer
use Moo;
@@ -17,35 +18,37 @@ Where to store the session files.
=cut
has session_dir => (
- is => 'ro',
- isa => Str,
- required => 1,
+ is => 'ro',
+ isa => Str,
+ default => sub { path('.', 'sessions') },
);
sub BUILD {
my $self = shift;
- if (! -d $self->session_dir) {
+ if (!-d $self->session_dir) {
mkdir $self->session_dir
- or croak "Unable to create session dir : ".$self->session_dir.' : '.$!;
+ or croak "Unable to create session dir : "
+ . $self->session_dir . ' : '
+ . $!;
}
}
sub _sessions {
my ($self) = @_;
my $sessions = [];
-
- opendir (my $dh, $self->session_dir)
- or croak "Unable to open directory ".$self->session_dir." : $!";
+
+ opendir(my $dh, $self->session_dir)
+ or croak "Unable to open directory " . $self->session_dir . " : $!";
while (my $file = readdir($dh)) {
next if $file eq '.' || $file eq '..';
if ($file =~ /(\w+)\.yml/) {
- push @{ $sessions }, $1;
+ push @{$sessions}, $1;
}
}
closedir($dh);
-
+
return $sessions;
}
@@ -62,31 +65,31 @@ sub _retrieve {
open my $fh, '+<', $session_file or die "Can't open '$session_file': $!\n";
flock $fh, LOCK_EX or die "Can't lock file '$session_file': $!\n";
- my $new_session = YAML::Any::LoadFile($fh);
+ my $data = YAML::Any::LoadFile($fh);
close $fh or die "Can't close '$session_file': $!\n";
- return $new_session;
+ return $data;
}
sub _destroy {
my ($self, $id) = @_;
my $session_file = $self->yaml_file($id);
- return if ! -f $session_file;
+ return if !-f $session_file;
- unlink $session_file
+ unlink $session_file;
}
sub _flush {
- my ($self, $session) = @_;
- my $session_file = $self->yaml_file( $session->id );
+ my ($self, $id, $data) = @_;
+ my $session_file = $self->yaml_file($id);
open my $fh, '>', $session_file or die "Can't open '$session_file': $!\n";
flock $fh, LOCK_EX or die "Can't lock file '$session_file': $!\n";
set_file_mode($fh);
- print {$fh} YAML::Any::Dump($session);
+ print {$fh} YAML::Any::Dump($data);
close $fh or die "Can't close '$session_file': $!\n";
- return $session;
+ return $data;
}
1;
View
116 t/session_config.t
@@ -0,0 +1,116 @@
+use strict;
+use warnings;
+use Test::More;
+
+use YAML;
+use Test::TCP 1.13;
+use File::Temp 0.22;
+use LWP::UserAgent;
+use HTTP::Date qw/str2time/;
+use File::Spec;
+
+sub extract_cookie {
+ my ($res) = @_;
+ my @cookies = $res->header('set-cookie');
+ for my $c (@cookies) {
+ next unless $c =~ /dancer\.sid/; # custom
+ my @parts = split /;\s+/, $c;
+ my %hash =
+ map { my ($k, $v) = split /\s*=\s*/; $v ||= 1; (lc($k), $v) } @parts;
+ $hash{expires} = str2time($hash{expires})
+ if $hash{expires};
+ return \%hash;
+ }
+ return;
+}
+
+my $tempdir = File::Temp::tempdir(CLEANUP => 1, TMPDIR => 1);
+
+Test::TCP::test_tcp(
+ client => sub {
+ my $port = shift;
+
+ my $ua = LWP::UserAgent->new;
+ $ua->cookie_jar({file => "$tempdir/.cookies.txt"});
+
+ my ($res, $cookie);
+
+ # set value into session
+ $res = $ua->get("http://127.0.0.1:$port/foo/set_session/larry");
+ ok $res->is_success, "/foo/set_session/larry";
+ $cookie = extract_cookie($res);
+ my $err;
+ ok $cookie, "session cookie set"
+ or $err++;
+ ok $cookie->{expires} - time > 3540, "cookie expiration is in future"
+ or $err++;
+ is $cookie->{domain}, '127.0.0.1', "cookie domain set"
+ or $err++;
+ is $cookie->{path}, '/foo', "cookie path set"
+ or $err++;
+ is $cookie->{httponly}, undef, "cookie has not set HttpOnly";
+ diag explain $cookie
+ if $err;
+
+ # read value back
+ $res = $ua->get("http://127.0.0.1:$port/foo/read_session");
+ ok $res->is_success, "/foo/read_session";
+ like $res->content, qr/name='larry'/, "session value looks good";
+
+ File::Temp::cleanup();
+ },
+ server => sub {
+ my $port = shift;
+
+ use Dancer;
+
+ get '/has_session' => sub {
+ return context->has_session;
+ };
+
+ get '/foo/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+
+ get '/foo/read_session' => sub {
+ my $name = session('name') || '';
+ "name='$name'";
+ };
+
+ get '/foo/destroy_session' => sub {
+ my $name = session('name') || '';
+ context->destroy_session;
+ return "destroyed='$name'";
+ };
+
+ setting appdir => $tempdir;
+ setting(
+ engines => {
+ session => {
+ Simple => {
+ cookie_name => 'dancer.sid',
+ cookie_domain => '127.0.0.1',
+ cookie_path => '/foo',
+ cookie_duration => 3600,
+## is_secure => 0, # can't easily test without https test server
+ is_http_only => 0, # will not show up in cookie
+ },
+ },
+ }
+ );
+ setting(session => 'Simple');
+
+ set(show_errors => 1,
+ startup_info => 0,
+ environment => 'production',
+ port => $port
+ );
+
+ Dancer->runner->server->port($port);
+ start;
+ },
+);
+done_testing;
+
+
View
24 t/session_engines.t
@@ -41,7 +41,10 @@ foreach my $engine (@engines) {
$res = $ua->get("http://127.0.0.1:$port/read_session");
like $res->content, qr/name='$client'/,
"session looks good for client $client";
-
+
+ $res = $ua->get("http://127.0.0.1:$port/clear_session");
+ like $res->content, qr/cleared/, "deleted session key";
+
$res = $ua->get("http://127.0.0.1:$port/cleanup");
ok($res->is_success, "cleanup done for $client");
@@ -55,7 +58,7 @@ foreach my $engine (@engines) {
my $port = shift;
use Dancer;
-
+
my @to_destroy;
hook 'engine.session.before_destroy' => sub {
@@ -73,13 +76,24 @@ foreach my $engine (@engines) {
"name='$name'";
};
+ get '/clear_session' => sub {
+ session name => undef;
+ return exists(session->data->{name}) ? "failed" : "cleared";
+ };
+
get '/cleanup' => sub {
- my $engine = engine('session');
- $engine->destroy(id => $_) for @{ $engine->sessions };
+ context->destroy_session;
return scalar(@to_destroy);
};
setting appdir => $tempdir;
+ setting(engines => {
+ session => {
+ $engine => {
+ session_dir => 't/sessions'
+ }
+ }
+ });
setting(session => $engine);
set(show_errors => 1,
@@ -87,7 +101,7 @@ foreach my $engine (@engines) {
environment => 'production',
port => $port
);
-
+
Dancer->runner->server->port($port);
start;
},
View
183 t/session_lifecycle.t
@@ -0,0 +1,183 @@
+use strict;
+use warnings;
+use Test::More;
+
+use YAML;
+use Test::TCP 1.13;
+use File::Temp 0.22;
+use LWP::UserAgent;
+use HTTP::Date qw/str2time/;
+use File::Spec;
+
+sub extract_cookie {
+ my ($res) = @_;
+ my @cookies = $res->header('set-cookie');
+ for my $c (@cookies) {
+ next unless $c =~ /dancer\.session/;
+ my @parts = split /;\s+/, $c;
+ my %hash =
+ map { my ($k, $v) = split /\s*=\s*/; $v ||= 1; (lc($k), $v) } @parts;
+ $hash{expires} = str2time($hash{expires})
+ if $hash{expires};
+ return \%hash;
+ }
+ return;
+}
+
+my $tempdir = File::Temp::tempdir(CLEANUP => 1, TMPDIR => 1);
+
+my @engines = qw(YAML Simple);
+
+if ($ENV{DANCER_TEST_COOKIE}) {
+ push @engines, "cookie";
+ setting(session_cookie_key => "secret/foo*@!");
+}
+
+foreach my $engine (@engines) {
+
+ diag "Testing engine $engine";
+ Test::TCP::test_tcp(
+ client => sub {
+ my $port = shift;
+
+ my $ua = LWP::UserAgent->new;
+ $ua->cookie_jar({file => "$tempdir/.cookies.txt"});
+
+ # no session cookie set if session not referenced
+ my $res = $ua->get("http://127.0.0.1:$port/no_session_data");
+ ok $res->is_success, "/no_session_data"
+ or diag explain $res;
+ my $cookie = extract_cookie($res);
+ ok !$cookie, "no cookie set"
+ or diag explain $cookie;
+
+ # empty session created if session read attempted
+ $res = $ua->get("http://127.0.0.1:$port/read_session");
+ ok $res->is_success, "/read_session";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ my $sid1 = $cookie->{"dancer.session"};
+ like $res->content, qr/name=''/, "empty session";
+
+ # set value into session
+ $res = $ua->get("http://127.0.0.1:$port/set_session/larry");
+ ok $res->is_success, "/set_session/larry";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+
+ # read value back
+ $res = $ua->get("http://127.0.0.1:$port/read_session");
+ ok $res->is_success, "/read_session";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ like $res->content, qr/name='larry'/, "session value looks good";
+
+ # session cookie should persist even if we don't touch sessions
+ $res = $ua->get("http://127.0.0.1:$port/no_session_data");
+ ok $res->is_success, "/no_session_data";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+
+ # destroy session and check that cookies expiration is set
+ $res = $ua->get("http://127.0.0.1:$port/destroy_session");
+ ok $res->is_success, "/destroy_session";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ is $cookie->{"dancer.session"}, $sid1, "correct cookie expired";
+ ok $cookie->{expires} < time, "session cookie is expired";
+
+ # shouldn't be sent session cookie after session destruction
+ $res = $ua->get("http://127.0.0.1:$port/no_session_data");
+ ok $res->is_success, "/no_session_data";
+ $cookie = extract_cookie($res);
+ ok !$cookie, "no cookie set"
+ or diag explain $cookie;
+
+ # set value into session again
+ $res = $ua->get("http://127.0.0.1:$port/set_session/curly");
+ ok $res->is_success, "/set_session/larry";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ my $sid2 = $cookie->{"dancer.session"};
+ isnt $sid2, $sid1, "New session has different ID";
+
+ # destroy and create a session in one request
+ $res = $ua->get("http://127.0.0.1:$port/churn_session");
+ ok $res->is_success, "/churn_session";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ my $sid3 = $cookie->{"dancer.session"};
+ isnt $sid3, $sid2, "Changed session has different ID";
+
+ # read value back
+ $res = $ua->get("http://127.0.0.1:$port/read_session");
+ ok $res->is_success, "/read_session";
+ $cookie = extract_cookie($res);
+ ok $cookie, "session cookie set"
+ or diag explain $cookie;
+ like $res->content, qr/name='damian'/, "session value looks good";
+
+ File::Temp::cleanup();
+ },
+ server => sub {
+ my $port = shift;
+
+ use Dancer;
+
+ get '/no_session_data' => sub {
+ return "session not modified";
+ };
+
+ get '/set_session/*' => sub {
+ my ($name) = splat;
+ session name => $name;
+ };
+
+ get '/read_session' => sub {
+ my $name = session('name') || '';
+ "name='$name'";
+ };
+
+ get '/destroy_session' => sub {
+ my $name = session('name') || '';
+ context->destroy_session;
+ return "destroyed='$name'";
+ };
+
+ get '/churn_session' => sub {
+ context->destroy_session;
+ session name => 'damian';
+ return "churned";
+ };
+
+ setting appdir => $tempdir;
+ setting(engines => {
+ session => {
+ $engine => {
+ session_dir => 't/sessions'
+ }
+ }
+ });
+ setting(session => $engine);
+
+ set(show_errors => 1,
+ startup_info => 0,
+ environment => 'production',
+ port => $port
+ );
+
+ Dancer->runner->server->port($port);
+ start;
+ },
+ );
+}
+done_testing;
+
+
View
10 t/session_object.t
@@ -5,9 +5,9 @@ use warnings;
use Test::More;
use Dancer::Core::Session;
-use Dancer::Session::Simple;
+use Dancer::SessionFactory::Simple;
-my $ENGINE = Dancer::Session::Simple->new;
+my $ENGINE = Dancer::SessionFactory::Simple->new;
subtest 'session attributes' => sub {
my $s1 = $ENGINE->create;
@@ -17,13 +17,11 @@ subtest 'session attributes' => sub {
my $s2 = $ENGINE->create;
isnt($s1->id, $s2->id, "IDs are not the same");
-
- ok( defined($s2->creation_time), "creation time is set");
};
my $count = 10_000;
subtest "$count session IDs and no dups" => sub {
- my $seen = {};
+ my $seen = {};
my $iteration = 0;
foreach my $i (1 .. $count) {
my $s1 = $ENGINE->create;
@@ -32,7 +30,7 @@ subtest "$count session IDs and no dups" => sub {
last;
}
$seen->{$id} = 1;
- $iteration++;
+ $iteration++;
}
is $iteration, $count,
Something went wrong with that request. Please try again.