Logging into a Django server with Cypress.io programmatically (without using UI)

大兔子大兔子 提交于 2019-12-05 12:54:23

Cypress automatically sends the cookie named "csrftoken" with the request, but Django expects the csrf token to be called "csrfmiddlewaretoken". Therefore, I had to get the token and pass it by hand as follows:

    cy.getCookie('csrftoken')
    .then((csrftoken) => {
        cy.request({
            method: 'POST',
            url: your_url_here,
            // "form: true" is required here for the submitted information to be accessible via request.POST in Django (even though the docs make it sound like a bare 'POST' request can be made without the "form: true")
            form: true,
            body: {
                csrfmiddlewaretoken: csrftoken.value,
                testing: true,
                obj_model: 'Customer',
                field_name: 'name',
                field_value: 'Customer - Testing'
            }
        })
        .then((result) => {
            expect(result.body.success).to.equal(true)
        })
        .then(() => {
            //additional processing here if needed
        })
    })

You can get the first CSRF token required for login using a HEAD request and looking at the cookies (no need to parse the page).

Also you can have your custom cy.login() return the token (asynchronously, so you need to use .then()) instead of having to call cy.getCookie('csrftoken') again if you need a token afterwards for POST requests and such:

Cypress.Commands.add('login', (username, password) => {

  return cy.request({
    url: '/login/',
    method: 'HEAD' // cookies are in the HTTP headers, so HEAD suffices
  }).then(() => {

    cy.getCookie('sessionid').should('not.exist')
    cy.getCookie('csrftoken').its('value').then((token) => {
      oldToken = token
      cy.request({
        url: '/login/',
        method: 'POST',
        form: true,
        followRedirect: false, // no need to retrieve the page after login
        body: {
          username: username,
          password: password,
          csrfmiddlewaretoken: token
        }
      }).then(() => {

        cy.getCookie('sessionid').should('exist')
        return cy.getCookie('csrftoken').its('value')

      })
    })
  })

})

Note: The token changes after login, therefore two cy.getCookie('csrftoken') calls.

Afterwards you can just use it in the following way in your tests (see https://docs.djangoproject.com/en/2.2/ref/csrf/ for why the header is needed):

cy.login().then((csrfToken) => {

  cy.request({
    method: 'POST',
    url: '/api/baz/',
    body: { 'foo': 'bar' },
    headers: { 'X-CSRFToken': csrfToken }
  })

})

To use Cypress to login programmatically with Django (i.e., without using the UI), the simplest solution is to change two words in the CSRF testing recipe that Cypress provides.

The two changes that I made in the below compared to the Cypress recipe at https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/logging-in__csrf-tokens/cypress/integration/logging-in-csrf-tokens-spec.js are:

  1. Changing _csrf to csrfmiddlewaretoken; and
  2. Changing $html.find("input[name=_csrf]").val() to $html.find("input[name=csrfmiddlewaretoken]").val()

Recipe updated for Django 2.2:

// This recipe expands on the previous 'Logging in' examples
// and shows you how to use cy.request when your backend
// validates POSTs against a CSRF token
//
describe('Logging In - CSRF Tokens', function(){
  const username = 'cypress'
  const password = 'password123'

  Cypress.Commands.add('loginByCSRF', (csrfToken) => {
    cy.request({
        method: 'POST',
        url: '/login',
        failOnStatusCode: false, // dont fail so we can make assertions
        form: true, // we are submitting a regular form body
        body: {
          username,
          password,
          csrfmiddlewaretoken: csrfToken // insert this as part of form body
        }
      })
  })

  it('strategy #1: parse token from HTML', function(){
    cy.request('/login')
      .its('body')
      .then((body) => {
        const $html = Cypress.$(body)
        const csrf  = $html.find("input[name=csrfmiddlewaretoken]").val()

        cy.loginByCSRF(csrf)
          .then((resp) => {
            expect(resp.status).to.eq(200)
          })
      })
  })

You are correct, Cypress is not sending the token in the body because it is undefined, because of the way you are using .get() on the input to get the token.

You are using .get() as a synchronous call, but it's actually async. This is because Cypress will intelligently retry finding the DOM element, and that takes an indeterminate amount of time. This is a core concept of Cypress that enables built-in tests. The Cypress documentation details this better than I can, so check that out here: https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Default-Assertions

How you access a property on an element in the DOM should be put in a callback, in your case:

cy.get("input[name='csrfmiddlewaretoken']").then($input=>{
    const hidden_token = $input.val()
    cy.request({
        method: 'POST',
        form: true,
        url: login_url,
        // body: {'username': 'guest', 'password': 'password', 'csrfmiddlewaretoken': cy.getCookie('csrftoken').value}
        body: {'username': 'guest', 'password': 'password', 'csrfmiddlewaretoken': hidden_token}
    })
})

...

Pro-tip: using Cypress's doc search will usually lend you what you need

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!