Uberspace 7 - Episode 3

In Uberspace 7 - Episode 2 haben wir das Thema Tests angerissen:

Nachdem Features definiert wurden sollte man anfangen, Tests zu schreiben und damit die Anforderungen an die Features genau definieren.

Aber wie sieht so ein Test eigentlich aus? Beispiel: Jeder User bekommt mit seinem Account eine Mailbox, die er mit seinem SSH-Kennwort abrufen kann. Ein Test dafür würde folgendermaßen aussehen:

  1. Einen User auf dem Server anlegen
  2. Ein SSH-Passwort setzen
  3. Eine Mail an User schicken
  4. Der User loggt sich per IMAP ein
  5. User holt Mail 1 per IMAP ab (Es kann nur eine Mail da sein, der User ist ganz frisch angelegt und das Postfach war vor der Testmail leer)
  6. Wir überprüfen den Inhalt der Mail auf einen bestimmten String
  7. Profit!

Diesen Test haben wir mit Ansible realisiert. Und jetzt mal Butter bei die Fische, so sieht das in Code aus: Die Variablen testuser und dummypassword haben wir bereits an anderer Stelle definiert, in ansible_fqdn steht der Hostname des Servers. <{{ testuser }}@{{ ansible_fqdn }}> ist also die E-Mail-Adresse des Testusers.

Schritt 1 und 2 läuft als User root auf der Maschine, so wie das Webinterface die Skripte auch ansteuern würde. Auf das expect-Modul gehen wir gleich noch ein.

- name: Create test account
  command: "/usr/local/sbin/uberspace-account-create -u {{ testuser }}"

- name: Set dummypassword for testuser
  expect:
    command: "passwd {{ testuser }}"
    responses:
      .*password: "{{ dummypassword }}"

Die Mail verschicken wir auch als root, wir prüfen ja in diesem Test nicht das Versenden von Mails, sondern den Empfang.

- name: Send mail to test account
  mail:
    host: 'localhost'
    port: 25
    to: "John Doe <{{ testuser }}@{{ ansible_fqdn }}>"
    subject: 'Test successful'
  ignore_errors: yes

Die weiteren Schritte werden als der Testuser ausgeführt:

- name: Check IMAP login and see if mail arrived
  expect:
    command: "openssl s_client -connect {{ ansible_fqdn }}:993 -quiet"
    responses:
      Dovecot ready: "a1 LOGIN {{ testuser }} {{ dummypassword }}"
      Logged in: "a2 LIST \"\" \"*\""
      List completed: "a3 EXAMINE INBOX"
      Examine completed: "a4 FETCH 1 BODY[]"
      Fetch completed: "a5 LOGOUT"
  register: imapconversation

Für alle, die das IMAP-Protokol nicht sprechen, gibt es z.B. bei anta.net eine kleine Spickhilfe. Das expect-Modul wartet auf bestimmte Ausgaben und antwortet dann mit definierten Antworten. Die komplette Ausgabe des Prozesses schreiben wir uns dann in die Variable imapconversation.

So weit, so gut. Jetzt müssen wir nur noch überprüfen, ob der Betreff der Mail ("Test successful" auch irgendwo in der Variable vorkommt:

- name: Check assertions
  assert:
    that:
      - "'Test successful' in imapconversation.stdout"

Und wenn das der Fall ist, ist der Test grün und wir können sagen: E-Mail-Empfang und Zugriff per IMAP klappt. Die Ausgabe von Ansible sieht dann so aus:

TASK [Create test account] *****************************************************
changed: [asteroid-dev]

TASK [Set dummypassword for testuser] ******************************************
changed: [asteroid-dev]

TASK [Send mail to test account] ***********************************************
ok: [asteroid-dev]

TASK [Check IMAP login and see if mail arrived] ********************************
changed: [asteroid-dev]

TASK [Check assertions] ********************************************************
ok: [asteroid-dev]

So soll's sein, läuft also.

Hier noch als Bonus der Inhalt von imapconversation (Wir haben auf unseren Testmaschinen ein selbst signiertes Zertifikat):

"depth=0 OU = IMAP server, CN = imap.example.com, emailAddress = postmaster@example.com\r\nverify error:num=18:self signed certificate\r\nverify return:1\r\ndepth=0 OU = IMAP server, CN = imap.example.com, emailAddress = postmaster@example.com\r\nverify return:1\r\n* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN] Dovecot ready.\r\r\na1 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS SPECIAL-USE BINARY MOVE] Logged in\r\r\n* LIST (\\HasNoChildren) \".\" INBOX\r\r\na2 OK List completed.\r\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\r\n* OK [PERMANENTFLAGS ()] Read-only mailbox.\r\r\n* 1 EXISTS\r\r\n* 1 RECENT\r\r\n* OK [UNSEEN 1] First unseen.\r\r\n* OK [UIDVALIDITY 1468428977] UIDs valid\r\r\n* OK [UIDNEXT 2] Predicted next UID\r\r\na3 OK [READ-ONLY] Examine completed (0.010 secs).\r\r\n* 1 FETCH (BODY[] {677}\r\r\nReturn-Path: <root>\r\r\nDelivered-To: test@localhost.localdomain\r\r\nReceived: (qmail 30064 invoked from network); 13 Jul 2016 16:56:17 -0000\r\r\nReceived: from unknown (HELO localhost.localdomain) (::1)\r\r\n  by ip6-localhost with SMTP; 13 Jul 2016 16:56:17 -0000\r\r\nContent-Type: multipart/mixed; boundary=\"===============4134123165338466742==\"\r\r\nMIME-Version: 1.0\r\r\nSubject: Test successful\r\r\nFrom: root\r\r\nX-Mailer: Ansible\r\r\nTo: John Doe <test@localhost.localdomain>\r\r\n\r\r\nMultipart message\r\r\n--===============4134123165338466742==\r\r\nContent-Type: text/plain; charset=\"us-ascii\"\r\r\nMIME-Version: 1.0\r\r\nContent-Transfer-Encoding: 7bit\r\r\n\r\r\nTest successful\r\r\n\r\r\n\r\r\n--===============4134123165338466742==--\r\r\n)\r\r\na4 OK Fetch completed.\r\r\n* BYE Logging out\r\r\na5 OK Logout completed."

Header von Wikimedia (CC BY-SA 3.0))