[Catalyst] Testing controller which require login.

Louis Erickson lerickson at rdwarf.net
Thu May 14 04:53:42 GMT 2009


First, let me say that I am pleased and impressed with the responses.  So 
often I ask a question on a mailing list and get little or nothing back.  
It is a real treat to see a place where that is not the case!  Complete 
sentences, coherent thoughts, *and* relevant answers to complex questions!  
Almost unbelievable!  Thank you all!

Let me also say that I did see the message from Peter Edwards 
<peter at dragonstaff.com>, and that I was looking at going down a path like 
that myself.  Your suggestion clarified how I might have done it.  It 
would have made my already-complex configuration even more so, but it 
should work.  Thank you for the useful technique of using the 
authentication system to get something like this working!

I saw mst's reply too, and appreciate it.  It was succinct and would have 
gotten me thinking in the right direction, which is often the hardest 
part.

I do have some questions to ask you, Tomas, about your incredibly useful 
and clear suggestions.

On Wed, 13 May 2009, Tomas Doran wrote:

> As usual in these parts, all questions welcome, although you implicitly
> volunteered to document anything not covered in the Manual/documentation
> already.

Just today I spent time writing up some documentation patches you pointed 
out I could contribute on my blog.  I'm going to try and keep adding 
things where I can.

Random question about that... if I have diffs to send to the list, are 
they preferred as part of the body or an attachment to the message so line 
wrap and such don't mangle them?

> Well volunteered in advance. ;_)

Where would these best go?  The Catalyst dev wiki?  I actually ask this 
about several different parts, now that I think of it.

On Wed, 13 May 2009, Tomas Doran wrote:
> Louis Erickson wrote:
> > My tests are currently based on the stubs generated by myapp_create,
> > and use Catalyst::Test.  I could move them to WWW::Mechanize::Catalyst if it
> > would help.
> 
> It generally makes things nicer, however I _do_ tend to just use
> Catalyst::Test to test login / logout / etc, as I run some basic 'does the app
> look sane' tests first up so that it's quick to just run the front end sanity
> checks when developing...
> 
> I've then got various Mech & Selenium tests (which are much more detailed and
> cover much more of the application / regressions etc) which run after that..

This sounds like "the right tool for the right job".  If the test is 
simple, then Catalyst::Test is up to it and you don't have to work any 
harder.  When it gets complicated, the additional features of Mechanize 
are useful, so change over.  Makes perfect sense!

> Technique 1:
> > I can't tell if Catalyst::Test supports cookies.  Does it?  Or do I need to
> > use WWW::Mechanize::Catalyst for that?
> 
> It doesn't support cookies per-se, but it's not hard to handle this yourself.
> For a nice neat example against the tutorial of testing login, see:
> 
> http://github.com/bobtfish/catalyst-app-tutorial-kiffin-authissues/blob/cbb7f692676ecd51805dd7cc6cf4393ff6c208c5/t/01app.t

I see this is checking the request header after the request is run.  To be 
able to generate a login cookie, I'd have to generate a cookie string, and 
add it to the request.  The documentation says the request only supports 
setting the host value, so I couldn't do that.  Am I missing something?

It looks like that would be reasonably straightforward, by adding
code to Catalyst::Test::_customize_request to handle the cookies.

Would that be a worthwhile change?  I'm not sure.  It might just be better 
to tell someone who needs that to use a different test method, such as 
Mechanize.  Myself, I think I'd rather change test harnesses than support 
the new code.

> > I've read some suggestions on how to go about this, and found a couple.
> 
> Technique 2:
> 
> > 1> Use WWW::Mechanize::Catalyst to log the user in
> 
> Works exactly like you'd expect :)

Yes, except that I can't get there from here.  That's the big quirk with 
the SSO and this configuration.  The application has no login page, it 
depends on something else to do that.  For a particular test instance, it 
is likely that the SSO server that could log the user in isn't running.

Some calls would have to be made using HTTP to another process, which may 
or may not be there, and others should be made locally.

If it weren't for that, Mechanize would be the easy answer.  This is why 
Technique 4 is interesting; a login page would be available from a URL 
offered by the app being tested, and Mechanize starts to look sensible.

> Technique 3:
> 
> > 2> Use the mock user in config to set a user who is logged in.
> 
> The second example is also fairly possible. I've never gone the whole hog and
> had an entirely fake user object, although I do have tests which apply a
> custom MYAPP_CONFIG environment variable, and have config which overwrites my
> authentication config
> 
> > I can set a mock user, but then that user is always logged in.  Can that
> > mock user be changed by the test scripts, so I can have them log out, log in
> > as an administrator, log in as an unprivileged user, etc, and test the pages
> > perform correctly?  If so, I've missed that - it would work, I think.
> 
> Well, sure:
> 
> package My::Mock::User;
> use strict;
> use warnings;
> use base qw/Catalyst::Authentication::User/;
> 
> our $id;
> 
> sub id { $id }
> 
> our @roles;
> 
> sub { @roles }
> 
> etc..
> 
> Then, assuming your tests and application are running in the same process
> space, you can just say:
> 
> $My::Mock::User::id = 1;
> $My::Mock::User::roles = qw/ admin /;

Considering my test environment already uses a separate configuration 
file, telling the authentication system to use a class like this would be 
straightforward.  This is probably the least work for me, but only because 
I already have the tests already using a different configuration.

Can a test get to Catalyst's configuration at run time, to change what 
users is mocked in by the authentication module, or would a class it can 
fiddle with like this be required.  Can I reach TestApp->config() from 
here?

> Technique 4:
> 
<snip: SSO as a library, add test code to allow logging in through the 
app>

> That's also viable in my opinion.
> 
> I mean - you're not trying to test the SSO itself, just how users who have
> logged in with SSO behave, right?
> 
> In fact, if you add t/lib/MyApp/Controller/Login/Testing.pm
> 
> then that controller will only be loaded in your tests which have said;
> 
> use FindBin ();
> use lib "$FindBin::Bin/lib";

This is brilliant, and I would never have thought of it.  It means it 
won't ever leak in to production code, and that it is there when the test 
apps need it.

<snip note that this allows test-only code>

> Technique 5:
> 
> > Can I just generate the cookie and put it in the cookie store directly
> > before the query is made to the application?  I can generate the cookie with
> > the SSO library.  I don't see a way to do that through either test tool.
> 
> Erm, yes - this will also work..
> 
> Don't think at the Catalyst::Test level here - your cookie is stored as a row
> in DBIC (assuming you are using DBIC), just generate a row with a known cookie
> value before you start testing :)

Then store it in the session table, with whatever backend it happens to 
be.  Mine is DBIC, but that's not the interesting part.  That would let me 
preload any cookie I felt like testing with and the application would 
naturally see it.

I'm not sure how those cookies are serialized and if it's going to be a 
pain to get the libraries to let me do it from a different place.

This would be one of my later choices.  It's really a special case of 
fixtures - it's just data to load before tests.  This is time-sensitive 
data which must be calculated, so I can't just store one.

> > I'm trying to learn and do this in a maintainable, sensible way.  Any help
> > you can give is highly appreciated.
> 
> I think that number 1 needs better docs in Catalyst::Test, and is a viable
> technique, but painful to write all your testing in Catalyst::Test.

I think you're right that number 1 does need some more documentation but 
I'm not sure it solves the problem I have.  It lets you see what cookies 
got set, but I don't think it lets you set them to control the logon.

It does let you test a bunch of other useful things - redirects, etc - 
that people might well benefit from seeing how to do.

Where would the best place for an elaboration of this technique be?  A 
cookbook entry in Catalyst::Test, so it is easily found?  A page on the 
Catalyst dev wiki?

> Number (2) just works. I'd note that I generally use Mech for walk-through
> type testing, and either Catalyst::Test to test specific regressions, or
> controller mocking if I _really_ need to to test stuff, and everything else is
> in my Model and can be tested in isolation..

#2 just works if the login page exists and can be reached.  Currently this 
is not guaranteed, and not easy for the test script to tell.  This is why 
I had to ask in the first place.  #4 would let #2 work in my application.

> 3 + 5 would probably win for maintainability in my opinion, as they'd be best
> implemented generically as additional modules which other people can reuse for
> the relevant bits of Auth and Session respectively, and I'll be happy to
> maintain the patches you supply.
> 
> I'm more keen on 3 actually, as I guess that'd be the easiest way to avoid
> testing authentication at all where you want to get on with testing the rest
> of your app.

3 does sound more maintainable to me; the user is simply by the test 
script and you don't have to mess about with faking a login/logout and 
depending on cookies or whatever.

I don't know how difficult it would be to implement 5 cleanly for every 
session store, and for every authentication system.  I'm not sure it's 
even possible, as not every authentication method uses cookies; the HTTP 
authenticator just checks the headers doesn't it, and the mocked user 
would work there, never bothering to check.

That doesn't mean general access to the stored session from a test script 
might not be useful, but I'm not sure it's generally useful.  Or am I 
misunderstanding how Catalyst stores the cookies... I thought they just 
went in to the session.

I just looked at the existing authentication plugins, hoping one of the 
current ones would allow this without new code.  I don't see one that can 
do so by itself.  Catalyst::Authentication::User::Mock, perhaps?  Or am I 
missing something?

> 4 makes sense in some situations - you need testing of login / roles etc, but
> given you've already done that, the rest of your app testing not having to
> mess around with that stuff could be cleaner - although I'd be tempted to
> think you should just factor the 'hand me back a logged in user' code out into
> a test library, or that you should mock the login code out, rather than adding
> extra login code.

4 lets me test application-specific behavior to user change, etc.  Part of 
me likes the idea, because it keeps the test flow as close to production 
as possible, but I'm not sure it's needed.  There's one somewhat complex 
set of routines that handle cookies timing out and things that wouldn't be 
exercised.  I'm not sure that's a real negative.

Of course, I can always do both, and use #4 to test those parts, and then 
a simple mock user to test the rest.  It might make tests faster.

The idea, though, of adding a controller at test-time, is brilliant.  If 
it gets written down, will it be horribly abused?  =)

Where would this idea best be described?  Catalyst dev Wiki?  Blog entry?  
Both?

Thank you again for this rich response.  I very much appreciate it, and if 
given some direction as to where things go, will try and put some words 
there to try and help someone else not have to ask.

-- 
Louis Erickson - lerickson at rdwarf.net - http://www.rdwarf.com/~wwonko/

All I want is a warm bed and a kind word and unlimited power
		-- Ashleigh Brilliant



More information about the Catalyst mailing list