Ícone do site Aprenda Golang

Como fazer fuzz test em requests HTTP (parte 2)

close up shot of keyboard buttons

Photo by Miguel Á. Padriñán on Pexels.com

Na primeira parte desse post, vimos como utilizar o fuzz test para gerar payloads automaticamente em nossos testes, o que nos ajudou a encontrar problemas quando o payload não vinha no formato que esperávamos.

Nessa segunda parte, vamos utilizar o fuzz para gerar os inputs que serão utilizados nos campos de um payload correto.

Utilizando o mesmo código do post anterior, antes de começar, vamos fazer uma pequena mudança na validação de e-mail. Além de validar se ele foi preenchido, vamos validar se o e-mail é válido.

Para isso, vamos criar um novo erro e um else para quando o valor do e-mail não esteja vazio.

...

ErrEmailRequired = errors.New("Email is required")

ErrEmailInvalid = errors.New("Email is invalid")

...

if p.Email == "" {
		return ErrEmailRequired
} else {
		rgx := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]+)*$")
		if !rgx.MatchString(p.Email) {
			return ErrEmailInvalid
		}
}
...

Agora, como boa parte do código do teste é igual ao anterior, vamos duplicar a função renomeando-a para FuzzValidateData.

func FuzzValidateData(f *testing.F) {
	testcases := []Person{
		{"Tiago Temporin", "tiago@aprendagolang.com.br", "1234", 32},
		{"Maria Castro", "maria.castro@algumdominio.com", "1Av6s#", 22},
		{"Daniela Fernandez", "dani@teste.com.br", "1234", 16},
	}

	for _, tc := range testcases { // seed
		data, _ := json.Marshal(tc)

		f.Add(data)
	}

	knowErrs := map[string]bool{
		ErrNameRequired.Error():   true,
		ErrEmailRequired.Error():  true,
		ErrPwdRequired.Error():    true,
		ErrPwdMinChars.Error():    true,
		ErrAgeMin.Error():         true,
		ErrInvalidPayload.Error(): true,
	}

	srv := httptest.NewServer(http.HandlerFunc(validate))
	defer srv.Close()

	f.Fuzz(func(t *testing.T, data []byte) {
		resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))
		if err != nil {
			t.Errorf("Error: %v", err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			if _, ok := knowErrs[string(body)]; ok {
				t.Skip(fmt.Sprintf("skiping knowing error: %s", body))
			}

			t.Errorf("Expected status code %d, got %d with error :%s", http.StatusOK, resp.StatusCode, body)
		}
	})
}

Modificando Fuzz Test

Como queremos que sejam gerados inputs para cada um dos campos que temos na nossa struct, precisamos modificar o looping onde fazemos o seed de dados para o fuzz.

for _, tc := range testcases {
    f.Add(tc.Name, tc.Email, tc.Password, tc.Age)
}

Feito isso, o próximo passo é alterar a função do f.Fuzz para “acomodar” os novos parâmetros.

f.Fuzz(func(t *testing.T, name, email, password string, age uint8) {

Dentro da função do f.Fuzz, como ela não recebe mais um slice bytes como parâmetro, antes de realizar a request, precisamos criar uma struct com os inputs recebidos e em seguida fazer o json.Marshal para gerar o slice de bytes que será enviado no POST.

p := Person{
	Name:     name,
	Email:    email,
	Password: password,
	Age:      age,
}

data, _ := json.Marshal(p)

O código completo ficará assim:

func FuzzValidateData(f *testing.F) {
	testcases := []Person{
		{"Tiago Temporin", "tiago@aprendagolang.com.br", "1234", 32},
		{"Maria Castro", "maria.castro@algumdominio.com", "1Av6s#", 22},
		{"Daniela Fernandez", "dani@teste.com.br", "1234", 16},
	}

	for _, tc := range testcases {
		f.Add(tc.Name, tc.Email, tc.Password, tc.Age)
	}

	knowErrs := map[string]bool{
		ErrNameRequired.Error():   true,
		ErrEmailRequired.Error():  true,
		ErrPwdRequired.Error():    true,
		ErrPwdMinChars.Error():    true,
		ErrAgeMin.Error():         true,
		ErrInvalidPayload.Error(): true,
	}

	srv := httptest.NewServer(http.HandlerFunc(validate))
	defer srv.Close()

	f.Fuzz(func(t *testing.T, name, email, password string, age uint8) {
		p := Person{
			Name:     name,
			Email:    email,
			Password: password,
			Age:      age,
		}

		data, _ := json.Marshal(p)

		resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))
		if err != nil {
			t.Errorf("Error: %v", err)
		}

		if resp.StatusCode != http.StatusOK {
			body, _ := ioutil.ReadAll(resp.Body)
			if _, ok := knowErrs[string(body)]; ok {
				t.Skip(fmt.Sprintf("skiping knowing error: %s", body))
			}

			t.Errorf("Expected status code %d, got %d with error :%s", http.StatusOK, resp.StatusCode, body)
		}
	})
}

Como temos 2 funções de fuzz e só podemos executar uma função por vez, precisamos adicionar um filtro na hora de executar o comando dos testes.

$ go test -fuzz ValidateData -fuzztime 5s .

Ao executar o comando acima, o seguinte erro foi exibido no terminal:

fuzz: elapsed: 0s, gathering baseline coverage: 0/65 completed
fuzz: minimizing 78-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 4/65 completed
--- FAIL: FuzzValidateData (0.10s)
    --- FAIL: FuzzValidateData (0.00s)
        main_test.go:99: Expected status code 200, got 400 with error :Email is invalid
    
    Failing input written to testdata/fuzz/FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4
    To re-run:
    go test -run=FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4
FAIL
exit status 1
FAIL    github.com/aprendagolang/httpfuzz       0.869s

Como podemos ver na mensagem de erro, o problema encontrado foi que o e-mail passado na request é inválido. Podemos confirmar isso ao abrir o arquivo que foi gerado em testdata/fuzz/FuzzValidateData/7b49f646b184e97f52133f9fc761792ec19f6fe185c357510535b999ff53dcc4 onde podemos ver os valores passados como Name, Email, Password e Age.

go test fuzz v1
string("0")
string("0")
string("0")
byte('\\x10')

Para resolver esse problema, basta adicionar o erro que já havíamos definido à nossa lista de erros conhecidos na função FuzzValidateData.

knowErrs := map[string]bool{
	ErrNameRequired.Error():   true,
	ErrEmailRequired.Error():  true,
	ErrEmailInvalid.Error():   true,
	ErrPwdRequired.Error():    true,
	ErrPwdMinChars.Error():    true,
	ErrAgeMin.Error():         true,
	ErrInvalidPayload.Error(): true,
}

Ao executar o comando novamente, o resultado foi o seguinte:

fuzz: elapsed: 0s, gathering baseline coverage: 0/82 completed
fuzz: elapsed: 0s, gathering baseline coverage: 82/82 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 6078 (2026/sec), new interesting: 7 (total: 89)
fuzz: elapsed: 6s, execs: 6678 (200/sec), new interesting: 7 (total: 89)
fuzz: elapsed: 6s, execs: 6678 (0/sec), new interesting: 7 (total: 89)
PASS
ok      github.com/aprendagolang/httpfuzz       6.276s

Com isso fechamos os posts, pelo menos por enquanto, sobre como utilizar Fuzz Test em requests HTTP.

Espero que tenham gostado.

Até a próxima!


Faça parte da comunidade!

Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.

Livros Recomendados

Abaixo listei alguns dos melhores livros que já li sobre GO.

Sair da versão mobile