Uwierzytelnianie w Sinatrze z Data Mapperem
W poprzednim poście dotyczącym Sinatry, stworzyliśmy najprostszą aplikację i pokazaliśmy jak można ją przetestować przy pomocy cucumbera. Ponieważ ciężko zrobić rozgrywki bez użytkowników, dziś dodamy do naszej ping-pongowej ligi, możliwość rejestracji i logowania.
Dla tych, którzy nie śledzili poprzednio odcinka, kod źródłowy z końca poprzedniego przykładu jest na githubie otagowany jako sinatra-pong 0.1.
Scenariusz rejestracji i logowania
Zgodnie z filozofią BDD zaczniemy od napisania scenariusza. Chcemy aby gość zarejestrował się, podając e-mail, hasło i jego potwierdzenie. Następnie powinien móc się zalogować i zobaczyć tekst, którego nie widzi jako gość. Plik ze scenariuszem nazwijmy features/registration.feature
.
Feature: Registration feature In order to join ping-pong league As a guest I want to register and log in Scenario: Register and log in Given I am the guest And I go to the home page Then I should see "sign up" When I follow "sign up" And I fill in "E-mail" with "user@example.com" And I fill in "Password" with "password" And I fill in "Password confirmation" with "password" And I press "Create account" Then I should see "Account created"
Pojedyńczy scenariusz możemy uruchomić poleceniem:
$>cucumber features/registation.feature (...) When I should see "sign up" expected #has_content?("sign up") to return true, got false (...) Failing Scenarios: cucumber features/registation.feature:7 # Scenario: Register and log in 1 scenario (1 failed) 9 steps (1 failed, 1 skipped, 6 undefined, 2 passed) You can implement step definitions for undefined steps with these snippets: When /^I follow "([^\"]*)"$/ do |arg1| pending # express the regexp above with the code you wish you had end When /^I fill in "([^\"]*)" with "([^\"]*)"$/ do |arg1, arg2| pending # express the regexp above with the code you wish you had end When /^I press "([^\"]*)"$/ do |arg1| pending # express the regexp above with the code you wish you had end
Zgodnie z oczekiwaniami, scenariusz wywalił się na trzecim kroku (nie mamy przecież nigdzie na stronie linku sign up
). 6 kroków jest niezdefiniowanych, przy czym widzimy, że tak naprawdę są to trzy rodzaje kroków (wypełnianie pola tekstowego, kliknięcie przycisku i podążenie za linkiem). Ostatni krok został pominięty, ponieważ wykonywanie scenariusza zostaje przerwane w przypadku wystąpienia błędu, lub niezdefiniowanego kroku.
Definiowanie brakujących kroków
Do pliku features/step_definistions/sinatra_steps.rb
dodamy teraz dwa wpisy. W pierwszym obsłużymy wypełnianie pól formularzy za pomocą methody fill_in
capybary.
When /^I fill in "([^\"]*)" with "([^\"]*)"$/ do |field, value| fill_in(field, :with => value) end
Drugi krok, to kliknięcie przycisku, tu przychodzi nam z pomocą funkcja press
. Więcej przydatnych metod można znaleźć w dokumentacji Capybary.
When /^I press "([^\"]*)"$/ do |button| click_button(button) end
Trzecim scenariuszem jest kliknięcie w link, analogicznie jak w poprzednim przypadku skorzystamy z metody click_link
.
When /^I follow "([^\"]*)"$/ do |link| click_link(link) end
Teraz co prawda nasz scenariusz nadal nie przechodzi testu, ale za to mamy zdefiniowane potrzebne do niego kroki.
Tworzymy pierwszy widok
Jednym z licznych plusów, tego, że napisaliśmy test przed stworzeniem kodu jest to, że w każdym momencie programowania, wiemy, co powinniśmy właśnie teraz napisać. Po prostu rozwiązujemy problemy zgłaszane przez cucumber’a tak długo, aż scenariusz nam się nie zazieleni. Wtedy wiemy, że skończyliśmy, a gratisowo mamy już napisany test. Ponieważ wynik testu informuje nas o tym, że nie widać linku “sign up”, wiemy, że teraz jest moment by go dodać.
Jak dotąd naszym widokiem był po prostu wynik ostatniego wyrażenia w akcji. Nie nie stoi na przeszkodzie, byśmy dodali widok w formie zwykłego pliku haml. W tym celu musimy zainstalować potrzebny gem.
gem install haml
A następnie załadować go i wykorzystać w głównym pliku aplikacji:
require 'rubygems' require 'sinatra' require 'haml' set :views, File.dirname(__FILE__) + '/views' get '/' do haml :index end
Ostatnie, co należy zrobić to utworzyć plik z widokiem. W Sinatrze domyślnie widoki trzymane są w podkatalogu views. Stwórzmy zatem views/index.haml
:
%p %a{ :href => '/signup'}sign up %p Hello ping-pong!
Jeśli ta składnia wydaje wam się dziwna, to czas najwyższy poznać Haml’a. Ponieważ nie mamy dostępu do railsowych helperów, nie korzystamy z dobrodziejstw w stylu link_to
i linki musimy generować ręcznie. Otrzymany na wyjściu fragment html’a, nie posiada poprawej struktury. Nic nie stoi jednak na przeszkodzie, byśmy dodali domyślny layout dla wszystkich stron. Wystarczy utworzyć plik views/layout.haml
:
!!! %html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang => "en"} %head %meta{'http-equiv' => "Content-Type", :content => "text/html; charset=utf-8"} %body = yield
Gdy teraz odpalimy testy, to zobaczymy, że krok, z którym jest problem, to:
And I fill in "E-mail" with "user@example.com"
Doszliśmy zatem do momentu, w którym należy dodać rejestrację.
Instalacja Sinatra Authentication
Uwierzytelnianie w Sinatrze najłatwiej jest zaimplementować wykorzystując gem sinatra-authentication. Co prawda, moglibyśmy pisać wszystko sami, ale nie wiem kiedy przy takim podejściu, pojawiłby się ten tutorial. By z tego gemu skorzystać, musimy go zainstalować, wraz z kilkoma zależnościami:
gem install dm rack-flash sinatra-authentication data_objects do_sqlite3
dm-core
, to gem zawierający DataMapper, lekki ORM, o który może być oparte sinatra-authentication.
Zgodnie z instrukcją na githubie, dołączmy potrzebne biblioteki do pliku pong.rb
:
require "dm-core" require "digest/sha1" require 'rack-flash' require "sinatra-authentication" use Rack::Session::Cookie, :secret => 'I like cake' use Rack::Flash
Po uruchomieniu naszych testów zobaczymy, że wszystkie kroki przechodzą oprócz ostatniego:
Then I should see "Account created, now you can log in"
Debugowanie widoków
Wiemy, że nie widzimy komunikatu o sukcesie, nie wiemy dlaczego. Warto by było zobaczyć podgląd strony. Mimo, że ten scenariusz nie jest skomplikowany, nie musimy wyklikiwać go w przeglądarce. Capybara
dostarcza odpowiednie metody. Jest to szczególnie przydatne, przy dłuższych, skomplikowanych scenariuszach.
W pliku features/registration.feature
dodajmy przed ostatnim krokiem następujący tekst:
Then show me the page
Zdefiniujmy potrzebny krok w features/step_definitions/sinatra_steps.rb
. Należy pamiętać, że metoda save_and_open_page
zapisuje widok jako plik html w głównym katalogu aplikacji. Warto zatem od czasu do czasu czyścić te pliki i pilnować, by nie dostały się do systemu kontroli wersji.
Then /^show me the page$/ do save_and_open_page end
Gdy teraz spróbujemy uruchomić testy, otworzy nam się w przeglądarce strona błędu, na której można znaleźć komunikat zalecający użycie bilobilu:
Adapter not set: default. Did you forget to setup?
I wszystko jasne, załadowaliśmy, ale nie skonfigurowaliśmy DataMappera. Najwyższy czas to zrobić.
Konfiguracja bazy danych
Ponieważ na razie będziemy testować naszą aplikację lokalnie, wystarczy nam trzymana w pamięci baza sqlite
. Dodajmy do pliku pong.rb
konfigurację bazy:
configure do DataMapper.setup(:default, 'sqlite3::memory:') end
Odpalamy testy i tym razem widzimy:
no such table: dm_users
Co staje się zrozumiałe, gdy uświadomimy sobie, że przecież nie stworzyliśmy bazy, ani nie przeprowadziliśmy migracji. Z pomocą przychodzi nam metoda auto_migrate
. Skoro na razie trzymamy naszą bazę w pamięci, to wystarczy, że po uruchomieniu aplikacji zmigrujemy bazę w pamięci. Zawartością bazy zajmuje się gem sinatra-authentication
, tabela z użytkownikami dla DataMappera nazywa się DmUsers
. Dodajmy zatem następującą linijkę po konfiguracji DataMappera.
DmUser.auto_migrate!
Teraz po uruchomieniu testu, otrzymujemy komunikat:
Then I should see "Account created" expected #has_content?("Account created") to return true, got false
Flash w Sinatrze
Sinatra-authentication korzysta z rack-flash, czyli prostej biblioteki dostarczającej funkcjonalność podobną do railsowych flashów. By z niej skorzystać zmodyfikujmy lekko nasz layout:
%body = flash[:notice] = yield
Uruchommy teraz testy, by zobaczyć, że wszystkie są zielone i przechodzą. Świetnie! Pora wyrzucić podgląd strony ze scenariusza i sprawdzić w przeglądarce, że rzeczywiście rejestracja jest możliwa. Jeśli wcześniej uruchomiliśmy serwer Sinatry, należy go zrestartować ( CTRL+C
, a potem ruby pong.rb
). Trzeba pamiętać o restartach serwera jeśli zmieniamy coś więcej niż tylko widok.
Ulepszenie scenariusza
Sama rejestracja to nie wszystko. Po rejestracji jesteśmy automatycznie zalogowani. Warto umożliwić wylogowanie. Dodatkowo, gdy jesteśmy zalogowani, głupio by było gdybyśmy mogli się zarejestrować. Zatem w zależności od tego, czy jesteśmy zalogowani, czy nie, powinniśmy widzieć właściwe linki. Zmodyfikujmy nieco nasz scenariusz:
Feature: Registration feature In order to join ping-pong league As a guest I want to register and log in Scenario: Register log out and log in Given I am the guest And I go to the home page Then I should see "sign up" And I should see "login" And I should not see "logout" When I follow "sign up" And I fill in "Email" with "user@example.com" And I fill in "Password" with "password" And I fill in "Confirm Password" with "password" And I press "Create account" Then I should see "Account created" And I should see "logout" And I should not see "login" And I should not see "sign up" When I follow "logout" Then I should see "Logout successful."
Po jego uruchomieniu widzimy, że brakuje nam kroku sprawdzającego, że danego elementu nie ma na stronie.
Then /^I should not see "([^\"]*)"$/ do |arg1| pending # express the regexp above with the code you wish you had end
W oparciu o poprzednie kroki w features/step_definitions/sinatra_steps.rb
, dodanie brakującego powinno być proste.
Then /^(?:|I )should not see "([^\"]*)"$/ do |text| page.should have_no_content(text) end
Uruchamiamy test ponownie i widzimy:
And I should see "login" expected #has_content?("login") to return true, got false
Oznacza to, że musimy dodać link do logowania na stronie głównej. Zmodyfikujemy views/index.haml
.
%p %a{ :href => '/signup'}sign up %a{ :href => '/login'}login %p Hello ping-pong!
Po kolejnym uruchomieniu testu widzimy, że:
And I should see "logout" expected #has_content?("logout") to return true, got false
Należy zatem dodać link umożliwiający wylogowanie.
%p %a{ :href => '/signup'}sign up %a{ :href => '/login'}login %a{ :href => '/logout'}logout %p Hello ping-pong!
W momencie, gdy już myśleliśmy, że warunki scenariusza są spełnione, okazuje się, że ostatnia zmiana, spowodowała błąd w jednym z wcześniejszych kroków.
And I should not see "logout" expected #has_no_content?("logout") to return true, got false
Co jest dość oczywiste, bo obecnie pokazujemy link do wylogowania niezależnie od tego czy jesteśmy zalogowani, czy nie. Z pomocą przychodzi nam jeden z helperów sinatra-authentication o wszystko mówiącej nazwie logged_in?
. Wykorzystajmy go w naszym widoku.
%p - if logged_in? %a{ :href => '/logout'}logout - else %a{ :href => '/signup'}sign up %a{ :href => '/login'}login %p Hello ping-pong!
Udało się, ogórek się zazielenił!
Podsumowanie
Napisanie testu wcześniej niż kodu, ułatwiło nam dodanie funkcjonalności. Przez cały czas pracy nad nią, wiedzieliśmy co dokładnie zrobić w danym momencie, a czynności które musieliśmy zrobić były na tyle małe i proste, że nie powinny sprawiać kłopotu. Jeśli nasza zmiana zepsuła test, od razu o tym wiedziliśmy i mogliśmy szybko wychwycić, co było przyczyną. Dodatkowo, w momencie gdy skończyliśmy, test na stworzoną funkcjonalność również był gotowy. Nie ma tu tak nielubianej przez programistów sytuacji, że mamy działający kod i trzeba do niego napisać testy. Nie, w tym wypadku testy już mamy.
W następnym odcinku zajmiemy się mniej wirtualną bazą i damy użytkownikom możliwość dodawania rozgrywek.
Dzisiejszy przykład na githubie: http://github.com/tjeden/sinatra-pong/tree/0.2