10 Minuten-Railssicherheit 0

Posted by fwoeck
on Friday, June 05

Den Vortrag “Security – What rails will and won’t do for you” von Rory McCune im Rahmen von Scotland On Rails 2009 habe ich zum Anlass genommen, den überfälligen Aspekt Sicherheit in meinem laufenden Railsprojekt anzugehen. Der Sprecher benennt XSS- und SQL-Injection-Angriffe als das bei weitem gewichtigste Problem in diesem Zusammenhang.

SQL-Injection

Solange man keine ungeprüften Daten, die potentiell aus Usereingaben stammen in Sql-Statements manuell verpackt, sorgt ActiveRecord dafür, dass alles automatisch sanitized wird. Insofern ist hier Entwarnung angesagt. Konkret heißt das, wenn direktes SQL erzeugt wird, dann NICHT so:

"name='#{@user.name}' and user_id='#{@user.id}'"

sondern SO:

["name='%s' and user_id='%s'", @user.name, @user.id]

Strategien gegen Cross-Site-Scripting

Hier schlägt man uns zwei Möglichkeiten vor:

  1. Prüfung von Userdaten bei der Eingabe
  2. Prüfung bei der Ausgabe

Für beide Varianten gibt es Plugins (siehe die Weblinks). Ich habe mich für die zweite Möglichkeit entschieden und safe-erb installiert:

script/plugin install git://github.com/abedra/safe-erb

Es wirft bei jeder un-sanitizeten Ausgabe eines Strings, der aus DB- oder File-IOs stammt eine Fehlermeldung. Das macht das unbeabsichtigte Übergehen von Lücken schwierig. Die h-Helfermethode Escape-t Tags:

<%=h @user.name %>
oder
<%= h(@user.name) %>

und sorgt dafür, dass tainted Strings untainted werden, d.h. durch die Prüfung kommen.

Zwei Tücken musste ich überwinden:

a) Daten die mit dem Rails-Fragmentcaching in memcached gespeichert werden, werden automatisch tainted, d.h. als potentiell unsicher eingestuft. Das ist nicht hilfreich, weil die Fragmente natürlich Tags enthalten. J. Roller schlägt in seinem Blog folgende Lösung vor, die die Ausgaben direkt in der memcached-Anbindung untaint-et. Sie wird z.B. in einem Initializer eingetragen:

1
2
3
4
5
6
7
8
9
class ActiveSupport::Cache::MemCacheStore   
  logger.info "Installing Memcache-safe_erb Patch"
  
  def read_with_untaint(*args)
    read_without_untaint(*args).untaint
  end
  
  alias_method_chain :read, :untaint
end

b) Bei der Einbettung von Javascript-Schnipseln gab es teilweise Probleme – auch hier darf man natürlich nicht die h-Methode von Rails benutzen, die Tags entfernt, sondern muss den Ergebnisstring untainten:

<%= (observe_field "project_active_#{project.id}", ..., :method => :post).untaint %>

Weblinks

  1. www.mccune.org.uk
  2. github.com/abedra/safe-erb
  3. code.google.com/sanitizeparams
  4. code.google.com/xssterminate
  5. www.owasp.org/Category:OWASP_Top_Ten_Project
  6. www.jroller.com/../fixing_safe_erb_with_memcached

ruby- (und Rails-) Projekte mit dem vim debuggen 0

Posted by fwoeck
on Wednesday, June 03

Anton Astashov hat kürzlich ein Plugin für vim veröffentlicht, mit dem man ruby- und Rails-Projekte aus dem Editor heraus debuggen kann. Mit

:Rdebugger

startet man den Mechanismus. Im Falle eines Railsprojektes wird dann ein Mongrel im development-Modus durch den Debugger gestartet. Auf den kann man wie üblich mit dem Webbrowser zugreifen. Hat man Breakpoints gesetzt, so hält der Prozess an der Stelle an und vim zeigt sie im Hauptfenster.

Nun kann man sich die aktiven Variablen ansehen und schrittweise durch den Quelltext gehen:

Installation

Damit die Sache funktioniert, muss der vim mit “1” antworten, wenn man ihn dies fragt:

:echo has("signs") && has("clientserver") && v:version > 700

Weder der eingebaute vim in Leopard noch der port-vim bringen im Moment eine Unterstützung für das Feature clientserver mit, aber die gvim- und MacVim-Varianten, die ich in Ubuntu und OsX ausprobiert habe, funktionierten beide klaglos.

Außerdem wird das ruby-debug-ide gem genötigt.

Zur Plugininstallation klont man das github-repo:

git clone git://github.com/astashov/vim-ruby-debugger.git

und kopiert dann einige Dateien in vims Pluginverzeichnis:

> cd ./vim-ruby-debugger
> cp -r vim/* ~/.vim/plugin/

Im Hilfetext des Plugins in vim stehen deutliche mehr Informationen, als auf der Hauptseite des github-Projekts:

:helptags ~/.vim/doc
:help ruby-debugger

Entgegen der Voreinstellung habe ich mir die Tastenkürzel auf die Alt-Taste verlegt. In der ~/.vimrc:

map <A-b>  :call g:RubyDebugger.toggle_breakpoint()<CR>
map <A-v>  :call g:RubyDebugger.open_variables()<CR>
map <A-m>  :call g:RubyDebugger.open_breakpoints()<CR>
map <A-s>  :call g:RubyDebugger.step()<CR>
map <A-n>  :call g:RubyDebugger.next()<CR>
map <A-c>  :call g:RubyDebugger.continue()<CR>
map <A-e>  :call g:RubyDebugger.exit()<CR>
map <A-d>  :call g:RubyDebugger.remove_breakpoints()<CR>

let g:ruby_debugger_fast_sender = 1

Die Letzte Zeile ist nötig, um die Benutzung eines Socket-Scripts in C zu aktivieren, dass vermutlich etwas schneller ist, als der übliche Weg (siehe Hilfetext).

Weblinks

github.com/astashov/vim-ruby-debugger

Hintergrunddienste ordentlich versorgen 0

Posted by fwoeck
on Friday, May 22

Für Railsprozesse, die im Hintergrund asynchron Dinge erledigen sollen, sind ein paar Dinge ganz hilfreich:

  1. Subroutinen zeitlich zu begrenzen und ggf. auftretende Fehler abzufangen
  2. den Dienst selbst auf Steuersignale horchen zu lassen (hier Sig-Term)
  3. adäquate Logeinträge zu prodizieren.

Der folgende Schnipsel zeigt an einer Pdf-Konvertierung, wie man’s machen kann. Er kann als Daemon (in Zusammenhang mit dem daemons-gem) gestartet werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env ruby

# You might want to change this
ENV["RAILS_ENV"] ||= "production"

require File.dirname(__FILE__) + "/../../config/environment"

$running = true
Signal.trap("TERM") do 
  $running = false
end

while($running) do
  
  pdfs = QAttachment.all.select {|qa| qa.content_type == 
           'application/pdf' && qa.answer.andand.value_str.blank?}

  pdfs.each do |f| 
    if File.readable?(f.public_filename)
      begin
        timeout(60) do
          f.answer.value_str = `pdftotext -enc UTF-8 -q "#{f.public_filename}" -`
        end
        f.answer.value_str = 'PDF-ERROR' if f.answer.value_str.blank?
      rescue Timeout::Error
        f.answer.value_str = 'PDF-ERROR'
      end
      f.answer.save
      ActiveRecord::Base.logger.info "Pdf-Conversion failed for Attachment
          #{f.answer.id} at #{Time.now}.\n" if f.answer.value_str == 'PDF-ERROR'
    else
      f.destroy
    end
  end

  sleep 10
end

Selenium, der ruby-Debugger und OsX 0

Posted by fwoeck
on Tuesday, May 12

Die Kombination cucumber/webrat/Selenium macht mir viel Spaß! Es ist sogar möglich während einer Selenium-Sitzung den ruby-debugger zu starten und mit dem Browser zu interagieren. Auf einer Ubuntu-Maschine könnte man folgende Steps definieren:

Given I follow the link "id=vorgangslink" 
And I follow the link "id=p2show" 
And I follow the link "id=formlink_1" 
And I type "ein" into "id=answer_7" 
And I wait 1 second
And the debugger is started
Then I should see "ein String"

Wobei der Debugger dann so aufgerufen wird:

1
2
3
Given /^the debugger is started$/ do
  debugger 
end

Im konkreten Lauf klappt das prima. Wenn man das Gleiche unter OsX tut, wirft das System einem allerdings eine Fehlermeldung um die Ohren, wenn man das selenium-Objekt anspricht, um den Browser zu steuern:

    Given I follow the link "id=vorgangslink" 
    And I follow the link "id=p2show" 
    And I follow the link "id=formlink_1" 
    And I type "ein" into "id=answer_7" 
    And I wait 1 second
/opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/lib/cucumber/ast/step_invocation.rb:33
status!(:passed)

(rdb:1) selenium.type_keys("id=answer_7", "hey")
NameError Exception: undefined local variable or method `selenium' for #<Cucumber::Ast::StepInvocation:0x36c9f28>

(rdb:1) list
[28, 37] in /opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/bin/...
   28          unless @skip_invoke || options[:dry_run] || exception ...
   29            @skip_invoke = true
   30            begin
...

(rdb:1)

Der Grund liegt darin, dass der OsX-Debugger im Gegensatz zu Linux am Ende eines Blockes bereits in den umliegenden Kontext wechselt, und damit das selenium-Objekt vergisst. Sorgt man durch Anfügen eines nichtstuenden Ausdrucks (hier true) dafür, das das debugger-Kommando nicht das letztze im Block ist, ist alles gut:

1
2
3
4
Given /^the debugger is started$/ do
  debugger 
  true
end

Die Google-Analytics API spricht mit dir 0

Posted by fwoeck
on Saturday, May 02

Kurz nach der Veröffentlichung der neuen Google-Analytics-API gibt es jetzt das gem garb, welches die Kommunikation damit erheblich vereinfacht. In diesem Beispiel frage ich die Werte visits, pageviews, etc. für den letzten Monat ab und übergebe sie dem jQuery-Plugin flot, das diese Daten auf eine recht hübsche interaktive Weise darstellt:

Hier folgen die Ergänzungen zu einer leeren Rails-2.3.2-Instanz. Das gesamte Projekt habe ich zum Clonen und Spielen auf Github eingestellt (s.u.).

Die Umgebung

Das gem wird aufgerufen (> 0.2), aus einem YAML-File werden die Accountinformationen eingelesen und eine Session mit Google etabliert.

In config/environment.rb:

1
2
3
4
5
6
7
8
9
10
...
Rails::Initializer.run do |config|
...
  config.gem "vigetlabs-garb", :lib => "garb"
...
end

google_path = File.join(RAILS_ROOT,"config","google.yml")
@@google_config = YAML.load(File.read(google_path))[RAILS_ENV]
Garb::Session.login(@@google_config['login'], @@google_config['password'])

Anmeldedaten

Das File config/google.yml enthält die Domaindaten:

1
2
3
4
5
6
7
8
9
development:
  domain: rnotes.bm.net
  login: me@bm.net
  password: p4ssw0rd

production:
  domain: rnotes.bm.net
  login: me@bm.net
  password: p4ssw0rd

Controller-Code

Wenn der Welcome-Index aufgerufen wird, wird die Datenabfrage im Controller assembliert und aufgerufen. Einer sauberen MVC-Kultur folgend, sollte man dies in einem richtigen Projekt vielleicht in ein Modell geben.

app/controllers/welcome_controller.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WelcomeController < ApplicationController

  def index
    profile = Garb::Profile.all.select {|p| p.title == 
                              @@google_config['domain']}.first
    report = Garb::Report.new(profile, :start_date => 
                              4.weeks.ago, :end_date => Time.now )

    report.metrics :pageviews, :visits, :visitors, :new_visits
    report.dimensions :date
    report.sort :date
    result = report.results

    @visitors = result.map(&:visitors)
    @visits = result.map(&:visits)
    @newvisits = result.map(&:new_visits)
    @pageviews = result.map(&:pageviews)
    @dates = result.map {|d| d.date.to_time.to_i * 1000 }
    @domain = @@google_config['domain']
  end

end

Hier ist bemerkenswert, wie die Datumsformate “CCYYMMDD”, die von Google als String kommen in Javascript-Timestamps umgerechnet werden, wie flot sie erwartet.

Die Diagrammdarstellung

Im Index werden die Ergebnisse ein ein Javascript eingefügt, das letztlich flot benutzt. Weitere Beispiele mit verschiedenen Optionen findet man auf der Website des Plugins.

Das app/views/welcome/index.html.erb-File:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<h1><%= @domain -%></h1>

<p><br /></p>

<div id="placeholder" style="width:600px;height:300px"></div>
<div id="overview" style="margin-left:200px;margin-top:20px;
                                           width:400px;height:50px"></div>

<script id="source" language="javascript" type="text/javascript">
$(function () {
    var d1 = [<%= i = -1; @visitors.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d2 = [<%= i = -1; @visits.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d3 = [<%= i = -1; @pageviews.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d4 = [<%= i = -1; @newvisits.map {|v| i += 1; 
                        "[#{@dates[i]}, #{v}]" }.join(", ") -%>];
    var d = [ { label: 'Visitors', data: d1 }, { label: 'Visits', data: d2 }, 
        { label: 'Pageviews', data: d3 },  { label: 'New visits', data: d4 } ];

    // helper for returning the weekends in a period
    function weekendAreas(axes) {
        var markings = [];
        var d = new Date(axes.xaxis.min);
        // go to the first Saturday
        d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 1) % 7))
        d.setUTCSeconds(0);
        d.setUTCMinutes(0);
        d.setUTCHours(0);
        var i = d.getTime();
        do {
            // when we don't set yaxis the rectangle automatically
            // extends to infinity upwards and downwards
            markings.push({ xaxis: { from: i, to: i + 2 * 24 * 60 * 60 * 1000 } });
            i += 7 * 24 * 60 * 60 * 1000;
        } while (i < axes.xaxis.max);

        return markings;
    }
    
    var options = {
        xaxis: { mode: "time" },
        selection: { mode: "x" },
        legend: { position: 'sw' },
        points: { show: true },
        lines: { show: true },
        grid: { markings: weekendAreas }
    };

    var plot = $.plot($("#placeholder"), d, options);

    var overview = $.plot($("#overview"), [d1, d2, d3, d4], {
        lines: { show: true, lineWidth: 1 },
        shadowSize: 0,
        xaxis: { ticks: [], mode: "time" },
        selection: { mode: "x" }
    });

    $("#placeholder").bind("plotselected", function (event, ranges) {
        // do the zooming
        plot = $.plot($("#placeholder"), d,
                      $.extend(true, {}, options, {
                          xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }
                      }));

        // don't fire event on the overview to prevent eternal loop
        overview.setSelection(ranges, true);
    });

    $("#overview").bind("plotselected", function (event, ranges) {
        plot.setSelection(ranges);
    });
});
</script>

Javascripts und JRails

flot selbst benötigt jQuery. Eine elegante und schnelle Art der Einbettung ist es, das jRails-plugin zu installieren – es bringt jQuery mit:

script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails

Das fügt die rechten Files in public/javascripts ein. Es fehlt nur noch das jquery.flot.js:

  1. jrails.js
  2. jquery-ui.js
  3. jquery.flot.js
  4. jquery.js

das html-Layout

Zum Schluss müssen die Scripte im Layout aktivert werden – app/views/layouts/application.html.erb:

1
2
3
4
...
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag 'jquery.flot' %>
...

Weblinks

  1. github.com/fwoeck/analytics
  2. introducing-garb-access-the-google-analytics-data-export-api-with-ruby
  3. github.com/vigetlabs/garb
  4. code.google.com/p/flot
  5. code.google.com/gdataReferenceDimensionsMetrics

Sehr cooles Debug- und Analysetool: rack-bug 0

Posted by fwoeck
on Wednesday, April 29

Die Rackifizierung greift um sich! Hier sehen wir eine Folge: Bryan Helmkamp hat grade sein an ein Django-Tool angelehntes rack-bug vorgestellt. Es ist eine Rackmiddleware, die ein Statusfenster in die laufende Website einblendet. Es gibt eine Menge sehr interessanter Möglichkeiten, z.B. die Auswertung

  1. der verbrauchten CPU-Zeiten en Detail
  2. der Session- und Cookievariablen
  3. der ENV- und Headerumgebung
  4. der SQL-Statements mit Backtrace, Explain, Timings und der Wiederholung einzelner Selects
  5. der Cachinhalte

und mehr!

Die Installation

... ist trivial (hier für Rails):

script/plugin install git://github.com/brynary/rack-bug.git

Anschließend ruft man die Middleware auf. In der config/initializers/middleware.rb:

require "rack/bug" 

ActionController::Dispatcher.middleware.use Rack::Bug,
  :ip_masks   => [IPAddr.new("127.0.0.1")],
  :secret_key => "KsdUrsfdY7sdfDEsdf52sdfhD4yWY+8z1",
  :password   => "p4ssw0rd"

Nun lässt sich das Tool auf der Unterseite /__rack_bug__/bookmarklet.html des Projekts aktivieren.

Weblinks

  1. www.brynary.com/rack-bug-debugging-toolbar-in-four-minutes
  2. github.com/brynary/rack-bug

multibox - Prototyp einer Multi-User DB-Verschlüsselung 0

Posted by fwoeck
on Sunday, April 26

Grade habe ich multibox auf Github publiziert. Um mir die doppelte Pflege zu sparen, verweise ich einfach auf den Eintrag dort und belasse es bei einer kurzen Beschreibung hier:

What it is

multibox is a quick prototype rails app that provides

  • a minimalistic user management via authlogic
  • an encrypted db-file store mechanism via strongbox and attachment_fu
  • a master key-phrase that encrypts the files
  • separate passwords/key files on a per user basis

The idea is to have

  • one central key/phrase to en-/decrypt all data without giving the main password to users
  • the ability to revoke single user’s access
  • the ability to change a user’s password without changing the master pass-phrase

Weblink

  1. github.com/fwoeck/multibox

Noch mal der <=>-Operator 0

Posted by fwoeck
on Wednesday, April 22

Die Methode sort ist so genügsam, dass es ihr reicht, wenn der <=>-Operator definiert ist. Damit lassen sich bequem angepasste Sortiermethoden – z.B. für ActiveRecord-Objekte – schreiben:

>> cs = Contact.all(:limit => 10)
=> [#<Contact id: "a372b1f0-c368-faf3-2dba-49db1a07e2a0", ...

>> cs.sort
NoMethodError: undefined method `<=>' for #<Contact: ...

>> class Contact
>>   def <=>(other)
>>     self.primary_address_postalcode.to_i <=> other.primary_address_postalcode.to_i
>>   end
>> end

>> cs.sort
=> [#<Contact id: "15890900-fa74-58ac-a5f6-49db1c37f4c6", ...

>> cs.map(&:primary_address_postalcode)
=> ["60320", nil, "01187", "10785", "70178", "61231", "10969", "60313", "34123", "65189"]

>> cs.sort.map(&:primary_address_postalcode)
=> [nil, "01187", "10785", "10969", "34123", "60313", "60320", "61231", "65189", "70178"]

sort_by

In einigen Fällen ist die Methode “sort_by” die schnellere Lösung: sie funktioniert ähnlich wie map. Hier klappt das nur eingeschränkt, denn sie benutzt den <=>-Operator direkt auf dem Tabelleninhalt und gibt auf, sobald einer davon NIL ist:

>> cs.sort_by(&:primary_address_postalcode)

NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.<=>

Tausche Prototype gegen jQuery 0

Posted by fwoeck
on Tuesday, April 21
  1. jQuery ist irgendwie unobtrusiv und schlank
  2. die Mobilplattform, mit der ich grade angefangen habe zu experimentieren benutzt jQuery
  3. für Rails gibt es jRails, wenn man will

Zeit für einen Wechsel! Ich fang’ mit dem laufenden Railsprojekt an.

Um die gewohnten Helfer in Rails und RJS weiter zu unterstützen und den Migrationsaufwand klein zu halten installiere ich jRails:

script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails

Manuell würde man jetzt die …/application.html.erb ändern:

1
2
3
<script src="/javascripts/jquery.js" type="text/javascript"></script>
<script src="/javascripts/jquery-ui.js" type="text/javascript"></script>
<script src="/javascripts/jrails.js" type="text/javascript"></script>

Falls man allerdings die übliche Zeile zum Einbetten der Skripte verwendet:


<%= javascript_include_tag :defaults, :cache => 'jquery' %>

ist es damit schon getan – anstelle der Prototype-Files werden nun die neuen geladen. Das

:cache => 'jquery'

veranlasst Rails in der Produktion die Skripte zu einer einzigen .js-Datei zusammenzufassen, um http-Requests zu sparen.

Aus unklaren Gründen funktioniert die Sache mit dem cache… in der Produktivumgebung gut, allerdings führt sie in meiner Selenium-Testumgebung zu Problemen. Gelegentlich tauchen solche Fehler auf:

$("#project_active_1").delayedObserver is not a function

Timingproblem? Deshalb deaktiviere ich das Caching erstmal wieder.

Einiges anders

Hier folgen ein paar Änderungen, die ich anbringen musste, um die Tests zu passieren:

Alle Helferaufrufe benötigen nun ein # für ids im Selektor – die waren vorher nicht nötig:


<%= link_to_function "Person anlegen", "$('#personselect').toggle();" %>

.up() wird .parent() oder .parents():

1
2
3
4
<div class="delme">
  <p>
    <%= link_to_function "<img alt='Bild' src='delete.png' />", 
        "$(this).up('.delme').remove()" %>
wird:
1
2
3
4
<div class="delme">
  <p>
    <%= link_to_function "<img alt='Bild' src='delete.png' />", 
        "$(this).parents('.delme').remove()" %>

.value= wird .val()


$('#answer_#{answer.id}_blank_value_date').value = '1';
wird:

$('#answer_#{answer.id}_blank_value_date').val('1');

Der Autocompleter streikt

Der autocompleter ist ja schon vor längerer Zeit in ein Plugin ausgegliedert worden und mein Viewcode

1
2
3
4
5
6
7
8
<script type="text/javascript">
  //<![CDATA[ 
   var answer_<%= answer.id -%>_auto_completer = new Ajax.Autocompleter(
    'answer_<%= answer.id -%>', 'answer_<%= answer.id -%>_auto_complete', 
    '/projects/<%= @project.id -%>/proforms/auto_complete_for_answer_id', 
    {method:'get'}) 
  //]]>
</script>

erzeugt eine Fehlermeldung “Ajax is not defined” im Firebug. Arghh, na ich denke, das wird eigener Post.

Weblinks

  1. ennerchi.com/projects/jrails
  2. dev.jqueryui.com/browser/tags/latest

Ein Rails-Plugin strippt AR-Attribute 0

Posted by fwoeck
on Thursday, April 09

Das Plugin strip-attributes entfernt Whitespaces vor und nach ActiveRecord-Einträgen. Perse leere Attribute werden ge-nil-t. Dadurch entfällt die lästige Prüfung und das manuelle Strippen. Achtung: das Plugin funktioniert über einen before_validation-Hook, d.h. es werden Attribute behandelt, bevor sie gespeichert werden – nicht nachdem sie gelesen wurden!

Die Aktivierung ist schnell getan:

script/plugin install git://github.com/rmm5t/strip_attributes.git

und für jedes Modell, das man behandeln möchte braucht es etwas wie:

1
2
3
4
5
6
7
8
9
10
11
class DrunkPokerPlayer < ActiveRecord::Base
  strip_attributes!
end

class SoberPokerPlayer < ActiveRecord::Base
  strip_attributes! :except => :boxers
end

class ConservativePokerPlayer < ActiveRecord::Base
  strip_attributes! :only => [:shoe, :sock, :glove]
end

Wie gesagt, der Strip geschieht vor der Validierung. Wenn man gelesene Attribute behandeln möchte, kann man sich damit behelfen, .valid? an dem Datensatz auszuführen:

>> u = User.find_by_firstname('Barbara ')
=> #<User oid: "1239070455218_JUser", isemployee: nil, login: ...

>> u.firstname
"Barbara " 

>> u.valid?
=> true

>> u.firstname
=> "Barbara"

Weblinks

github.com/rmm5t/strip_attributes

Schritt Vier: Selenium-Support 0

Posted by fwoeck
on Saturday, March 28

Wie sieht’s mit Selenium in cucumber und Kumpels aus (webrat kann ja kein JScript/Ajax)? Dies sind die Schritte zur Aktivierung:

Ein paar gems

gem install selenium-client
gem install bmabey-database_cleaner

Der database-cleaner besorgt im Selenium-Modus das Rollback der Datenbank. Im webrat-Modus tut Rails dies selbst durch stornierte Transaktionen.

Firefox-Binary verlinken

In Ubuntu muss ein SymLink auf das aktuelle Firefox-Binary gesetzt werden:

sudo ln -s /usr/lib/firefox-3.0.8/firefox /usr/bin/firefox-bin

Weil er direkt auf die Binaryversion zeigt, zerbricht er leider bei Updates.

Anpassung der cucumber-Umgebung

Da sicher nicht alle Features Ajax/JScript benötigen, ist es pfiffig, zwei getrennte Umgebungen – plain (nur webrat) und enhanced (mit selenium) – zu definieren. Dazu legt man eine entsprechende cucumber.yml in’s Rails-root(!) und schafft die Filestruktur wie unten angegeben:

cucumber.yml:

1
2
3
4
5
default: -r features/support/env.rb -r features/support/plain.rb \
           -r features/step_definitions features/plain

selenium: -r features/support/env.rb -r features/support/enhanced.rb \
           -r features/step_definitions features/enhanced

feature-Tree:

features(devel) > tree
.
|-- enhanced
|   `-- 533386.feature
|-- plain
|   `-- 533386.feature
|-- step_definitions
|   |-- session_steps.rb
|   `-- webrat_steps.rb
|-- support
|   |-- enhanced.rb
|   |-- env.rb
|   |-- paths.rb
|   `-- plain.rb

Hier habe ich zu Testzwecken einfach das gleiche 533386.feature-File für beide Umgebungen benutzt.

Die support/enhanced.rb:

1
2
3
4
5
6
7
8
9
10
11
12
require 'spec/expectations'
require 'selenium'
require 'webrat'
 
Webrat.configure do |config|
  config.mode = :selenium
  config.application_environment = :test
end
 
require 'database_cleaner'
require 'database_cleaner/cucumber'
DatabaseCleaner.strategy = :truncation

Wenn man hier das environment auf “test” setzt, benötigt man keine explizite “selenium”-Railsumgebung mehr.

Die support/env.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
require 'cucumber/rails/world'
require 'cucumber/formatters/unicode'
Cucumber::Rails.bypass_rescue

require 'webrat'

Webrat.configure do |config|
  config.mode = :rails
end

require 'cucumber/rails/rspec'
require 'webrat/core/matchers'
require 'factory_girl'

Es wird hier vor allem das “use_transactional_fixtures” der normalen Konfiguration entfernt. Dies kommt in die support/plain.rb:


Cucumber::Rails.use_transactional_fixtures

Aufruf der beiden Modi

Auf geht’s:

cucumber

allein ruft nun die plain-Features mit webrat auf und das Kommando

cucumber -p selenium

entsprechend die enhanced-Features mit Selenium. Ein manuelles Starten des selenium-Dienstes ist nicht vorher notwendig.

Wichtig: wie sich gezeigt hat, ist das benutzte webrat-Plugin (Version pre 0.4.3) noch buggy. Dieser Patch beseitigt ein Problem, bei dem einzelne Tests zufällig mit 0ms-Timeouts abbrachen..

Integration mit pickler/Tracker

Die Integration mit dem Pivotal Tracker via pickler funktioniert auch in den Unterordnern – man ruft die Features jetzt nur mit Pfad auf:

pickler pull features/enhanced/539953.feature

Hurra, sehr cool das!

Weblinks

  1. cucumber/setting-up-selenium
  2. webrat/selenium.rb
  3. http://seleniumhq.org/download/
  4. groups.google.com/group/webrat
  5. bmabey/database_cleaner
  6. examples/selenium_webrat
  7. http://gist.github.com/83635

Der dritte Schritt: Factory-Girl 0

Posted by fwoeck
on Saturday, March 28

Die ersten Integrationstests im marsXPress sind in der Test-DB gelaufen, in die zuvor Datensätze auf konventionellem Weg geschrieben wurden (User.create…) – dies ist jetzt gefixtured! Hier wird gezeigt, wie die User-Stories und deren Schrittdefinitionen gestrickt sind und wie die benutzen Modelle mit Factory-Girl definiert sind.

Factories

Hier habe ich mal die Gerippe der Modelle User, Roll und Member eingesetzt. Die AR-Definitionen sind so gesetzt, dass

User ... has_many :rolls, :through => :members

Member hat nur zwei Spalten user_id und roll_id. Die Factory-Definitionen liegen zentral in spec/factories.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'factory_girl'
 
Factory.define :user do |u|
  u.email 'me@bm.net' 
  u.password 'p4ssw0rd'
  u.password_confirmation 'p4ssw0rd'
end

Factory.define :roll do |u|
  u.name "System Services"
end

Factory.define :member do |u|
  u.association :user
  u.association :roll
end

Mit der Methode association zeigt man an, dass es sich um einen Proxy handelt. Allgemein gelten die Parameter als Defaults und können bei der Instanziierung überschrieben werden.

env.rb

In der env.rb sollte man entgegen der Voreinstellung die Test-Umgebung erzwingen, um Reibungsverluste zu vermeiden:

ENV["RAILS_ENV"] = "test"

Die Feature-Datei

Die Cucumber-Features sind weitergeführt, wie im letzten Artikel diesbzgl. beschrieben:

1
2
3
4
5
6
7
8
9
10
11
12
13
Feature: Users can log in and out
  In order to access the website
  As a user
  I can login to and logout from my account

  Scenario: admin-user logs in to the website
    Given I am not yet logged in
    When I visit "/"
    And I enter the admin's username and password
    And I click on "anmelden"
    Then I should see "Du hast Dich angemeldet."
    And I should see "Abmelden (Admin)"
...

Die Step-Definitionen

Hier wird’s spannender:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Before do
  Given "the admin-user account exists"
end

Given /^I am not yet logged in$/ do
  visit new_session_path
  response.should contain("Anmelden")
end

When /^I visit "([^\"]*)"$/ do |arg1|
  visit arg1
end

When /^I click on "([^\"]*)"$/ do |arg1|
  click_button arg1
end

When /^I enter the admin's username and password$/ do
  fill_in "login", :with => "admin"
  fill_in "password", :with => "p4ssw0rd"
end

Given /^the admin\-user account exists$/ do
  unless User.find_by_login("admin")
    roll = Factory.create(:roll, :name => 'Admins', :id => 1)
    user = Factory.create(:user, :ldap => false, :login => 'admin', 
               :name => 'Admin User', :roll_ids => [roll.id], :id => 1)
    member = Factory.create(:member, :roll => roll, :user => user)
    user.activate!
  end
end

...

Wie man sieht, kann man die ids der Datensätze festlegen falls erforderlich, dies geht mit AR nicht so direkt ( :id => 1 ).

Cucumber-Lauf

Und schließlich kommt der Lauf:

pplpool(devel) > cucumber features/533386.feature:7

# http://www.pivotaltracker.com/story/show/533386
Feature: Users can log in and out
  In order to access the website
  As a user
  I can login to and logout from my account

  Scenario: admin-user logs in to the website
    Given I am not yet logged in
    When I visit "/" 
    And I enter the admin's username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (Admin)" 

1 scenario
6 passed steps

Weblinks

  1. webjam, Factory-Beispiele mit Cucumber und rSpec
  2. Video-Trailer

Der zweite Schritt: Integrationstests mit cucumber 0

Posted by fwoeck
on Friday, March 27

Der nächste witchtige Schritt ist getan: die ersten Integrationstests mit cucumber und webrat sind für das laufende Projekt geschrieben:

pplpool(bdd) > RAILS_ENV="test" cucumber features/533386.feature

# http://www.pivotaltracker.com/story/show/533386
Feature: Users can login
  In order to get access to the website
  As a user
  I first must login

  Scenario: Admin-User logs in to the website
    Given I am not yet logged in
    And the admin-user account exists 
    When I visit "/" 
    And I enter the admin's username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (Admin)" 

  Scenario: User logs in to the website
    Given I am not yet logged in
    And a user account exists
    When I visit "/" 
    And I enter a valid username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (You)" 
2 scenarios
14 passed steps

Hurra!

Hurra!

Mir gefällt noch nicht, dass es diese beiden

And a ... user account exists

gibt, die eigentlich keine sichtbaren Verhalten der Views beschreiben – je nun, im Moment fällt mir dazu noch nichts besseres ein.

Zaghafte agile Schritte 0

Posted by fwoeck
on Thursday, March 26

Während meiner Annäherungen an das BDD, die hier noch weiter undokumentiert sind, kommt mir ein gem unter, das User-Stories eines laufenden Tracker-Projekts mit cucumber-Features eines Rails-Projekts synchronisiert.

Es heisst pickler und installiert sich wie folgt:

sudo gem install tpope-pickler
echo "api_token: <das apitoken hier>" > ~/.tracker.yml
cd /pfad/zum/rails-root/
echo "project_id: 11773" > ./features/tracker.yml
echo "ssl: false" >> ./features/tracker.yml

Stories verwalten

Damit kann man die Stories durchsuchen, starten, etc.:

> pickler search
533386 :/ 1 Users can login

> pickler start 533386

> pickler comment 533386 "give a nifty comment"

Wie’s aussieht, werden die Änderungen in der Website aktiv gepusht, sodass angemeldete User diese sofort wahrnehmen – sehr hübsch!

Features erzeugen

Wenn ich aus den Stories Cucumber-Features erzeugen möchte, “pull”e ich die zugehörige Nummer und es wird ein Template gleichen Namens erstellt:

> pickler pull 533386

> cat features/533386.feature 
# http://www.pivotaltracker.com/story/show/533386
Feature: Users can login
  User can (must) login to the website to be able to do things

So sieht man auch, wozu die Description in den Stories gut ist – sie gibt die sprachliche Feature-Umschreibung. Eine bessere Variation davon wäre vielleicht:

Feature: Users can login
  In order to to do things on the website
  As a frontend user
  I want to be able to log in

Weblinks

  1. pivotaltracker.com
  2. tpope/pickler

Multibyte-Zeichen richtig behandeln 0

Posted by fwoeck
on Thursday, March 26

In den Ruby 1.8er-Versionen kann die richtige Behandlung von Multibyte-Strings knifflig sein:

>> "Niels Müller".reverse
=> "rell\274?M sleiN"

Rails bietet eine Proxymethode mb_chars an, die die Zeichen verkapselt und damit begehbar macht:

>> "Niels Müller".mb_chars.reverse
=> #<ActiveSupport::Multibyte::Chars:0x9f0ceb4 @wrapped_string="rellüM sleiN">

>> "Niels Müller".mb_chars.reverse.to_s
=> "rellüM sleiN"