# Introdução

{% hint style="info" %}
Este livro ainda está sendo escrito, logo, algumas mudanças podem vir a serem feitas sem aviso prévio.
{% endhint %}

Nos últimos anos tenho trabalhado em projetos de médio e grande escala, tendo que lidar com grandes cargas de dados e integrações bizarras que parecem ter saído direto de um filme de terror.

Nesse tempo, tive a sorte de trabalhar com grandes pessoas, que tem grandes ideias e que resolviam problemas do dia-a-dia de forma elegante. Absorvi parte desse conhecimento e junto com isso, coloquei algumas ideias em prática. Algumas deram errado, outras se saíram melhor e pude registrar em meu cérebro como uma boa técnica.

O que foi registrado em minha memória estou passando para esse livro.&#x20;

Antes de falar o que vamos ver aqui, quero dizer uma coisa que vai aumentar sua produtividade instantaneamente. Não é nada mágico ou energético.  Também não gosto de frases de coach, então, não esperem por isso no livro.&#x20;

A frase mágica para você render o dobro na metade do tempo (sim, há uma referencia irônica aqui) é:

> &#x20;Você não precisa fazer tudo que precisa entregar.&#x20;

*Puff*. Parte dos problemas sumiram em uma nuvem de fumaça.&#x20;

Mas como poderíamos fazer algo assim?

### Contexto do mundo web

Estamos em meados de 2024, e já faz alguns invernos que as empresas .com nasceram. Junto delas, diversos serviços foram criados. Quer um serviço de pagamento? Você pode usar o *Mercado pago*. Mandar e-mail? *Mailgun*. Ou você quer traduzir um texto de qualquer língua para o português? *Google Translator*.&#x20;

Todos esses serviços estão disponíveis na web e você pode utilizar. Duvido muito que não tenha acessado o *translate.google.com*. Mas, eu tenho uma coisa interessante para te falar. Todos esses serviços podem ser integradas via API.

Você não precisa mais criar um mecanismo de cobrança, alguém já fez isso, você só precisa aprender a integrar ele. Agora, imagine a quantidade de serviços disponíveis na internet. Você acha mesmo que alguém não pensou na solução que você esta fazendo? Porque não da uma chance e procura antes um serviço que te ajude a não fazer essa parte e adiantar o teu trabalho sem drenar sua vida. A sim, não sabe onde procurar certo. Beleza, vou adiantar algo aqui para você.&#x20;

* <https://rapidapi.com/hub>
* <https://apidog.com/apihub/>
* <https://hub.mundipagg.com/>

Sim, eu não preciso falar mais nada, acredito que você entendeu o recado.

Mas não é só café na vida do programador. Muitas vezes integrar com outros serviços pode dar dores de cabeça. Autenticação, rate limit, indisponibilidade, lentidão, e vários outros. Mas calma, respira fundo. A ideia desse livro é te ajudar a atravessar essa floresta de problemas, para no fim, criarmos um serviço de qualidade.

O que você acha de criar um serviço juntando vários outros serviços? Você pode ate ganhar um dinheiro com isso. É uma prática comum. *Ahem... Ahem...* OK, vamos lá. Espero ter fisgado sua atenção e espero que vocês gostem.&#x20;


# Sobre o autor

<figure><img src="/files/hxhlcRp3uoQYtos2eqoU" alt="" width="320"><figcaption></figcaption></figure>

[Iago Effting](https://twitter.com/iagoEffting) é desenvolvedor desde 2009. Atuou em projetos dentro e fora do Brasil utilizando a linguagem Elixir. Considerado um profissional generalista, trabalhou com mobile (dart/flutter), web (JavaScript/CSS) e back-end (NodeJS, Go, Ruby, PHP e Elixir), tendo hoje seu ponto de estudo construção de APIs e padrões de projeto.

Sua grande paixão é em integrações de API's e desenvolvimento guiado a testes.


# O valor desse livro

Não acho que viverei da escrita criativa e nem técnica, mas isso não me impediu de começar a me aventurar nesse mundo. É algo que gosto de fazer e tenho um carinho especial por frases bem elaboradas. Programar é algo próximo disso. Códigos bem elaborados que deixam a leitura fluida e funcionamento fácil é algo incrível. Quando comecei a escrever senti uma vontade de ver a página final, mas isso custa um alto preço em minha vida. Tempo. E é por isso que tenho que falar sobre o valor desse livro.

Chamo de valor propositalmente. O que venho pedir é que pense o quanto esse livro te ajudou ou vai ajudar. O que ele mudou na forma de pensar ou te empolgou em fazer um projeto. Esse é o valor que peço que analisem com carinho, não deve levar muito tempo.

Com base nessa análise, vou disponibilizar algumas formas de precificar. Sim, você leu direito. Quero que você precifique. Isso será meu norte em saber a qualidade do que pareceu uma boa ideia e o impácto que realmente causou. Não sou escritor profissional e não tenho um editor para analisar meu texto. Tudo aqui é feito por mim, em meio a tarefas do trabalho, tarefas como esposo e tarefas como pai. Não é algo fácil, mas me sinto motivado a seguir.

## Algumas formas que você pode me incentivar a continuar

### Me pagando um café

Preciso de energia para escrever, e você pode me pagar o café. Vou disponibilizar meu pix aqui para mandar qualquer valor que seu coração mandar. Pode ser R$ 5, R$ 10 ou qualquer valor que você quiser. Isso realmente me deixará feliz.

<div align="left"><figure><img src="/files/eYDzkd322EkkNg6PqMG7" alt="" width="217"><figcaption><p>QR para PIX</p></figcaption></figure></div>

**Chave aleatória PIX**

84ece977-a04b-47e3-8209-2ffbb3c1e44b

### Seguindo minhas redes e compartilhando o conteúdo

Sei que muitos não possuem dinheiro para esbanjar dessa forma. Que temos famílias e sonhos a alcançar. Para vocês, peço apenas que compartilhem o conteúdo. Não só o livro como as redes que participo

{% embed url="<https://cafecomelixir.com.br>" %}

{% embed url="<https://dev.to/iagoeffting>" %}

{% embed url="<https://www.youtube.com/@IagoEffting>" %}

Também possuo uma newsletter semanal =D

{% embed url="<https://cafecomelixir.substack.com/>" %}


# Por que elixir?

Já passei por diversas linguagens e frameworks. PHP, Ruby, Go Lang, C#, etc. Hoje, meu xodó é o Elixir e não devido a surfar na onda do hype (o que acho já deve estar passando do elixir), mas devido a grandes vantagens que ele entrega por padrão. Mas não vai eu que irei te convencer a usar. Vou deixar isso para pessoais mais qualificadas.

* [What Is: Elixir](https://www.youtube.com/watch?v=2uvScmCrouk)
* [Why is EVERYONE Learning Elixir](https://www.youtube.com/watch?v=0C0vIXFOLGM)
* [A linguagem](https://aprenda.cafecomelixir.com.br/primeiros-passos-em-elixir/linguagem)

{% hint style="info" %}
Caso queira aprender a linguagem, tenho um outro livro que pode ser do seu agrado \
\
<https://aprenda.cafecomelixir.com.br/>
{% endhint %}


# Como ler este livro

Estou criando o livro para poder ser lido do começo ao fim. Isso quer dizer que ele é incremental. Mas isso não deve limitar sua imaginação em relação a leitura.&#x20;

O objetivo desse livro é nos preparar para diversas adiversidades na integração com serviços externos. Pode ser usado o exemplo do livro, como também, pode fazer um paralelo com um outro serviço que voce queira se integrar. Claro, provavelmente não será a mesma forma de se integrar, mas você terá boas ideias para lidar com isso aqui. Talvez tenha que avançar no livro e depois voltar. O importante é você deixar ser absorvido pelo livro e absorver ele do jeito que achar mais confortável e prático. A leitura deve ser fluida e não deve contem partes propositalmentes difíceis.&#x20;

Espero que consiga ler do jeito que preferir. Talvez um dia tenha até em video, vai saber.&#x20;

Por hora, uma boa leitura.


# Sobre o conteúdo do livro

Comentei na introdução que estamos em uma era onde a integração de serviços geram novos produtos. Peças de lego ilimitadas para criarmos novas experiencias.&#x20;

Imagine misturar:

* Dados de hospedagem baratas
* Dados de passagem barata
* Atividades ao ar livre
* Clima do local da viagem
* Agenda pessoal de viagem

Você pode usar esses dados para oferecer atividades em Florianópolis a um usuário que gosta de atividade no sol, fazendo o planejamento para ele com base nos dias que ele quer tirar férias e adicionando possíveis eventos por perto.

Seria um trabalho gigantesco implementar tudo isso, time gigante deve estar a postos e um infra ainda mais cara. Porém, você pode utilizar serviços já criados:

* *Booking*
* *Google Travel*
* *Google Callendar*
* *Real-Time Events Search*

Agora basta conectar tudo isso.

A proposta deste livro é entendermos como utilizar serviços externos e lidar com suas limitações, provendo qualidade ao nosso projeto. Um produto completo tem essa cara:

<figure><img src="/files/rM5X4ueAlw0Kx5GvN6oa" alt=""><figcaption></figcaption></figure>

Isso quer dizer, podemos lidar com N integrações de serviços externos ao mesmo tempo que entregamos respostas para consumidores que querem utilizar nosso serviço.

Nesse livro lidaremos apenas com a parte de integração. Não mostrarei como criar um serviço REST nem como os contextos podem se comunicar. Existem livros e artigos que falam sobre isso. Para deixar mais claro, trabalharemos nessa camada:

<figure><img src="/files/a7m34b9aD07AUEkXEP56" alt=""><figcaption></figcaption></figure>

Se você já realizou alguma integração, sabe de limitações que podem ser imposta pelo serviço conectado:

* Autenticação
* Rate Limit
* Lentidão
* Indisponibilidade

Como podemos criar um produto, se somos reféns da integração? Claro que tudo tem um limite, mas podemos nos defender e criar um serviço com boa qualidade, seguindo algumas regras e padrões.

Bem, vamos lá.


# Instalando o Elixir

Existem algumas formas de fazer a instalação, dependendo do sistema operacional ou ferramenta que você está utilizando. Para a forma mais básica de instalação pode ser acessado o site onde possui um [guia de instalação](https://elixir-lang.org/install.html) bem simples.

{% hint style="info" %}
**Na versão atual do livro estamos usando a versão 1.15 do elixir.**
{% endhint %}


# Criando um projeto

Para criar um projeto em elixir é de uma facilidade absurda. A própria linguagem tem um CLI para realizar a geração do mesmo. Para isso, basta utilizar o comando `mix new [nome do projeto]` e tudo que precisamos será gerado.

```
mix new coffee_shop
```

```sh
> mix new coffee_shop
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/coffee_shop.ex
* creating test
* creating test/test_helper.exs
* creating test/coffee_shop_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd coffee_shop
    mix test

Run "mix help" for more commands.
```

Tendo rodado o comando, entraremos dentro do projeto `cd coffee_shop` e rodaremos o teste para ver se tudo esta funcionando

```
mix test
```

```sh
> mix test
Compiling 1 file (.ex)
Generated coffee_shop app
..
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures
Randomized with seed 251121
```

Com isso, podemos seguir nos estudos.


# Iniciando

Como dito anteriormente, a proposta aqui é conseguir se integrar em serviços de terceiros. Para isso, precisamos de um terceiro a se conectar. Irei usar o exemplo já passado. Ele é simples e atende o objetivo do estudo.&#x20;

```
https://api.sampleapis.com/coffee/hot
```

Com o avançar do livro, lidaremos com serviços mais complexos. Caso se sinta entediado pela simplicidade, avance no sumário e encontre algo que te deixe empolgado.&#x20;

Agora precisamos criar nossa primeira integração. Mas antes disso, vou explicar sobre a ferramenta que utilizaremos para tal fim.

Vamos lá.


# Tesla


# O que é o Tesla

O Tesla é um cliente HTTP. Isso significa que ele consegue fazer requisições HTTP para outros serviços, obter a resposta e trata-la.&#x20;

Vamos a um exemplo prático. Nos precisamos de uma lista de cafés quentes para mostrar para nosso usuário. O problema é que não queremos ter que alimentar uma base de dados para isso. Para isso, podemos utilizar um serviço que já tenha os dados.&#x20;

O endpoint que usaremos é o `https://api.sampleapis.com/coffee/hot` e você pode abrir ele diretamente do browser, uma vez que ele usa o método GET.

<figure><img src="/files/qCYsSQR1Y54lBcUAyvR1" alt=""><figcaption><p><a href="https://api.sampleapis.com/coffee/hot">https://api.sampleapis.com/coffee/hot</a></p></figcaption></figure>

Temos a lista de cafés e não levou nem um minutos para ver o resultado. A minha pergunta agora é, como colocar isso em seu código. Precisamos de um cliente HTTP para se comunicar com o serviço.

Um cliente HTTP significa que ele se comunica utlizando requisições HTTP.  Esse link que fiz vocês abrirem é uma requisição HTTP do verbo GET para o endpoint especificado.

Em elixir, temos algumas bibliotecas que podemos utilizar. Se você entrar no [hex.pm](https://consumindo-apis-com-elixir.cafecomelixir.com.br/) e procurar por HTTP, vários bibliotecas irão aparecer, como o mint, hackney, fintch, gun, tesla, req, etc.&#x20;

Eu utilizarei o Tesla pela simplicidade.&#x20;

Para ele funcionar realizar a requisição ao serviço que estamos querendo conectar, basta fazer isso.

```elixir
Tesla.get("https://api.sampleapis.com/coffee/hot")
```

```elixir
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://api.sampleapis.com/coffee/hot",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Tue, 09 Apr 2024 16:50:14 GMT"},
     {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""},
     {"server", "cloudflare"},
     {"content-length", "8671"},
     {"content-type", "application/json; charset=utf-8"},
     {"x-powered-by", "Express"},
     {"access-control-allow-origin", "*"},
     {"x-ratelimit-limit", "5000"},
     {"x-ratelimit-remaining", "4986"},
     {"x-ratelimit-reset", "1712681756"},
     {"x-content-type-options", "nosniff"},
     {"cf-cache-status", "DYNAMIC"},
     {"report-to",
      "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=ARi0FWJNsQ8cXdIj82hbPEudl4%2Fj6ftR4olGo1dGTyQAunl1wpmbiwq9WLpRRe0BhRNEDhdKm4JpqOphKKpF1nt9UtgX57QgBwCKs%2BevTc8WeIYez3w0S11oqdtTNjU4COXzeEc%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},
     {"nel",
      "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},
     {"cf-ray", "871bfeb7fffd1acb-GRU"},
     {"alt-svc", "h3=\":443\"; ma=86400"}
   ],
   body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" <> ...,
   status: 200,
   opts: [],
   __module__: Tesla,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
```

Com isso, temos a resposta programaticamente. Isso quer dizer que posso fazer o que eu quiser com esse código. Claro, Tesla trás trambém os *headers*, *status*, *body* e *url* para termos tudo em nossas mãos e fazer um bom trabalho com isso.&#x20;

Com isso deve ter ficado claro o que é o cliente http e porque utilizaremos Tesla. Agora, vamos por a mão na massa.


# Instalando Tesla

A instalação do Tesla é encontrada na [página](https://hexdocs.pm/tesla/readme.html#installation) da biblioteca Para adicionar ao seu projeto, você deve adicionar a dependência em seu projeto.

<pre class="language-elixir" data-title="mix.exs" data-line-numbers><code class="lang-elixir">defp deps do
  [
<strong>    {:tesla, "~> 1.4"},
</strong><strong>    {:hackney, "~> 1.17"}, # Iremos usar Hackney por trás
</strong>  ]
end
</code></pre>

Feito isso, basta gerir as dependências e a instalação estará pronta.

```sh
mix deps.get
```

## Configuração

Configuração segue simples. Precisamos apenas configurar o Adaptador HTTP que iremos usar. Para fazer isso, precisamos criar o arquivo `config/config.exs.`

{% code title="config/config.exs" %}

```elixir
import Config

config :tesla, adapter: Tesla.Adapter.Hackney
```

{% endcode %}

Parabéns, você está apto a utiliza o Tesla em seu projeto :fireworks:


# Criando o Client

O cliente é parte que cuidará de sua integração. Terá todas as configurações necessárias para conseguirmos nos conectar a um serviço. Também teremos algumas estratégias como politica de *Retry* e *Cache*. Ele vai ser nosso contexto para o serviço.  Fazendo assim, deixando as coisas mais organizadas.

Vamos do início. Precisamos criar nosso cliente para se comunicar com o [SampleApis](https://sampleapis.com/), um serviço de exemplos de APIs que estamos usando como base em nossos estudos. Ele será responsável em realizar a requisição ao *serviço* e trazer os dados. Também irá lidar com possíveis erros.&#x20;

Em nosso primeiro exemplo, queremos trazer a lista do elixir da vida, também chamado de cafézinho quente. Para isso precisamos saber onde estamos pisando.&#x20;

Vamos fazer uma lista de objetivos. Gosto de listas bem definidas.

O serviço a ser usado encontrasse na URL `https://api.sampleapis.com/coffee/hot`, e seu verbo *HTTP* é *GET*. Isso quer dizer, se você abrir esse link em seu navegador você vai ver uma lista de itens no formato em JSON de cafezinhos quente, como vimos [nesse exemplo](https://app.gitbook.com/o/emUaSshwpl9F47G6Ig0P/s/egTKEnXxNB5i4htSH2x4/~/changes/46/construindo-um-cliente-usando-tesla/o-que-e-o-tesla-rever-titulo).

Agora precisamos delegar essa ação para o Tesla. Nossa lista de tarefa então será

* Obter lista de cafés quentes
* Testar resposta de sucesso com os cafés quentes

### Implementação

Criaremos um novo arquivo dentro de uma pasta ***integrations***. Esse será a pasta do contexto de integrações. Tudo referente a ele viverá ali. Seguirei a ideia de [Screaming Archtecture](https://medium.com/@mubashirhussain29/the-screaming-architecture-story-08750691291f) para ter um norte.

```
mkdir lib/coffee_shop/integrations/coffee
```

Em sua pasta, criamos um arquivo chamado *client.ex* referente ao cliente do serviço.

{% code title="lib/coffee\_shop/integrations/coffee/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Client do
 
end
```

{% endcode %}

Nele vamos criar nossa função de requisição, chamarei ela de `all_hot_coffees/0`

{% code title="lib/coffee\_shop/integrations/coffee/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Client do
  def all_hot_coffees do
  
  end
end
```

{% endcode %}

Agora precisamos entender onde devemos requisitar o serviço

```
GET https://api.sampleapis.com/coffee/hot
```

Vamos traduzir isso para código.&#x20;

Precisamos que o tesla execute uma chamada **GET** para esse recurso. O *Tesla* possui funçoes de acordo com o verbo que precisamos `Tesla.post/2`, `Tesla.get/1`, `Tesla.delete/1` e `Tesla.put/2` . Obviametne, precisamos do `Tesla.get/1` onde seu parâmetro é o recurso que iremos requisitar, sendo `https://api.sampleapis.com/coffee/hot`.

```elixir
Tesla.get("https://api.sampleapis.com/coffee/hot")
```

Uma boa leitura vindo do nosso requisito, certo? Vamos colocar esse código para funcionar.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  def all_hot_coffees do
<strong>    Tesla.get("https://api.sampleapis.com/coffee/hot")
</strong>  end
end
</code></pre>

Perfeito. Uma coisa que tenho que te falar. Tesla possui o conceito de [middlewares](https://hexdocs.pm/tesla/Tesla.Middleware.html). Eles são comportamentos que executam antes de um requisição acontecer. Podemos usar ele para simplificar um pouco o código. Como por exemplo, configurar nossa url base, ao invez de colocar tudo diretamente na função. Para conseguir usar os middlewares de forma limpa, podemos usar uma macro do Tesla e transformar nosso Client em um Tesla.Client. Isso cria uma dependência direta. Não gosto muito disso, mas iremos por esse lado para explorar todo o potencial do Tesla. Com a macro, podemos utilizar plug para configurar os middlewares e utilizaremos o middleware [BaseUrl](https://hexdocs.pm/tesla/Tesla.Middleware.BaseUrl.html). Com a macro e o middleware, podemos remover um pouco de codigo.

{% code title="lib/coffee\_shop/integrations/coffee/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://api.sampleapis.com"

  def all_hot_coffees do
    get("/coffee/hot")
  end
end
```

{% endcode %}

Nossa função ficou mais simples. Vamos continuar assim por um tempo. Com tudo devidamente configurado, podemos rodar nossa função de requisição pelo terminal iterativo. Acesse o terminal.

```
iex -S mix
```

Chame a função croiada diretamente.

```elixir
CoffeeShop.Integrations.Coffee.Client.all_hot_coffees()
```

```elixir
iex(1)> CoffeeShop.Integrations.Coffee.Client.all_hot_coffees()
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://api.sampleapis.com/coffee/hot",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 10 Apr 2024 20:41:08 GMT"},
     {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""},
     {"server", "cloudflare"},
     {"content-length", "8671"},
     {"content-type", "application/json; charset=utf-8"},
     {"x-powered-by", "Express"},
     {"access-control-allow-origin", "*"},
     {"x-ratelimit-limit", "5000"},
     {"x-ratelimit-remaining", "4988"},
     {"x-ratelimit-reset", "1712781684"},
     {"x-content-type-options", "nosniff"},
     {"cf-cache-status", "DYNAMIC"},
     {"report-to",
      "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=vdnOEvbwkmUy92e5RTJWIlW8yFKQNctDQEAemw5dNp5KfO8XJWWpDUx7hg%2B%2F99CuPdhTNsilv0kbqA554IsuLjdGRG9kys%2FGX1SpC08NAxF8nrw67hqNs6lhnGsQffS6doqnODg%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},
     {"nel",
      "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},
     {"cf-ray", "87258e520d730319-GRU"},
     {"alt-svc", "h3=\":443\"; ma=86400"}
   ],
   body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" <> ...,
   status: 200,
   opts: [],
   __module__: CoffeeShop.Integrations.Coffee.Client,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
```

Que coisa linda não? Você fez sua primeira requisição a um serviço externo programaticamente. Você consegue ver a lista de cafés e usar em sua aplicação. Para confirmar que tudo está vindo como queremos, você pode ir no atributo *body* e notará que tem uma resposta em JSON.&#x20;

Muito mais fácil que você imaginava não? Porém, ainda temos alguns objetivos

* ~~Obter lista de cafés quentes~~
* Testar a resposta de sucesso com os cafés

### Teste

Estamos usando um serviço externo para obter dados. Isso nos poupa tempo em relação a desenvolver uma solução. Mas precisamos garantir que nossa implementação sempre venha a funcionar. Para isso, podemos realizar testes programaticamente.

Iremos criar nosso arquivo de teste replicando o caminho da implementação e adicionaremos a base para nossos cenários a serem cobertos.

{% code title="test/coffee\_shop/integrations/coffee/client\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
   
    end
  end
end
```

{% endcode %}

Estamos utilizando o [*ExUnit* ](https://hexdocs.pm/ex_unit/ExUnit.html)para nossos testes e com ele, vamos criar nossas afirmações:

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  
<strong>  alias CoffeeShop.Integrations.Coffee.Client
</strong>  
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
<strong>      assert {:ok, _response} = Client.all_hot_coffees()
</strong>    end
  end
end
</code></pre>

Rodando o teste

```sh
mix test test/integrations/coffee/client_test.exs
```

```sh
> mix test test/integrations/coffee/client_test.exs
.
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 test, 0 failures

Randomized with seed 209276
```

Precisamos também garantir que a resposta seja um Tesla.Env, estrutura que o Tesla devolve em suas respostas. Vamos adicionar ao nosso teste.

{% code title="test/integrations/coffee/client\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client

  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
      assert {:ok, %Tesla.Env{}} = Client.all_hot_coffees()
    end
  end
end

```

{% endcode %}

```sh
mix test test/integrations/coffee/client_test.exs
```

```sh
> mix test test/integrations/coffee/client_test.exs
.
Finished in 0.7 seconds (0.00s async, 0.7s sync)
1 test, 0 failures
```

### Conclusão

Nosso primeiro *test* esta funcionando, isso quer dizer, menos um item na lista. Fácil não? Para que um livro desse? =D

* ~~Obter dados quando a resposta for um sucesso~~
* **Tratar resposta quando a resposta for um erro**
* Devemos ter acesso fácil a lista de cafés

Nosso próximo passo é testar o comportamento quando a resposta de nosso serviço volta um erro.  A pergunta mais comum aqui é, como diabos eu farei gerar um erro nisso se o serviço não é meu?

Hora do mock.


# Estruturando resposta

Conseguimos nos conectar ao serviço e obtemos a resposta de nosso pedido.&#x20;

Requisitamos e essa estrutura foi apresentada.

<pre class="language-elixir"><code class="lang-elixir">{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://api.sampleapis.com/coffee/hot",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Thu, 11 Apr 2024 18:41:16 GMT"},
     {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""},
     {"server", "cloudflare"},
     {"content-length", "8671"},
     {"content-type", "application/json; charset=utf-8"},
     {"x-powered-by", "Express"},
     {"access-control-allow-origin", "*"},
     {"x-ratelimit-limit", "5000"},
     {"x-ratelimit-remaining", "4999"},
     {"x-ratelimit-reset", "1712861765"},
     {"x-content-type-options", "nosniff"},
     {"cf-cache-status", "DYNAMIC"},
     {"report-to",
      "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=t8jefu1i1%2FKUiH7aYPEOaufCEApMVHn4aHootP6XuJhDF0L5XC6UmY33iprTNR2IXOnbBsNgN7E1a%2Fsuc5rZ9Pm9Qu%2Fp3gtobw4sxZmvrxTR8NXYOdajBzbwmJtJAw%2FG5eu1Pcg%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},
     {"nel",
      "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},
     {"cf-ray", "872d1c1dce920323-GRU"},
     {"alt-svc", "h3=\":443\"; ma=86400"}
   ],
<strong>   body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&#x26;fit=crop&#x26;q=60&#x26;w=800&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&#x26;fit=crop&#x26;q=60&#x26;w=800&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" &#x3C;> ...,
</strong><strong>   status: 200,
</strong>   opts: [],
   __module__: CoffeeShop.Integrations.Coffee.Client,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
</code></pre>

Essa é a resposta que o *Tesla* nos trás.&#x20;

A primeira coisa que gosto de fazer, é tirar o Tesla de vista. Gosto de criar um estrutura para recebermos a resposta e poder trafegar pelo nosso contexto sem a dependência do *Tesla* se espalhando.&#x20;

Para isso criaremos uma estrutura simples com dois atributos

* *status -> responsável por guardaro status da resposta*
* *body -> responsável por guardar os dados da requisição*

Vamos criar um novo modulo chamada response dentro do contexto coffee

{% code title="lib/integrations/coffee/response.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map
end
```

{% endcode %}

Então, podemos utilizar a estrutura:

```elixir
%Response{
  status: 200,
  body: "{data: null}"
}
```

Eu gosto disso para facilitar o entendimento e deixar as coisas mais homogeneas.&#x20;

Para utilizar essa estrutura, vamos criar uma função chamada `build/1` nesse mesmo arquivo `Response` e lidar com a resposta do Tesla, criando nosso ponto de transição. Para pegar os dados que queremos, vamos utilizar *pattern matching* e extrair as informações ao mesmo tempo que garantimos que nossa resposta virá com a estrutura esperada. Em seguida, iremos utilizar nossa estrutura de Response para devolver os dados extraídos.

<pre class="language-elixir" data-title="lib/integrations/coffee/response.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map

<strong>  def build({:ok, %Tesla.Env{status: status, body: body}}) do
</strong><strong>    response = %__MODULE__{
</strong><strong>      status: status,
</strong><strong>      body: body
</strong><strong>    }
</strong><strong>    
</strong><strong>    {:ok, response}
</strong><strong>  end
</strong>end
</code></pre>

Adicionamos o `:ok`, como parte da convenção do elixir, você pode ver mais sobre nesse [link](https://aprenda.cafecomelixir.com.br/conceitos/convencoes#tupla-ok-result-e-error-reason).

Elegante não? Que acha de um teste para isso?

{% code title="test/integrations/coffee/response\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    test "build from Tesla.Env structure" do
      tesla_response = {:ok, %Tesla.Env{status: 200, body: "something here"}}

      assert {:ok, %Response{}} = Response.build(tesla_response)
    end
  end
end

```

{% endcode %}

Rodaremos o teste para ver ele passando

```sh
mix test
```

```sh
> mix test test/integrations/coffee/response_test.exs
.
Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 test, 0 failures
```

Temos agora um forma estruturada de resposta. Precisamos alterar nosso cliente, esperamos que ele responda nossa nova estrutura. Vamos no teste primeiro. Nosso teste está dizendo que a resposta esperada é um `Tesla.Env`, mas agora possuímos nosso própria estrutura. Vamos trocar para o `Response`.

Também podemos fazer a validação do status. Nesse caso receberemos um status 200

{% code title="test/integrations/coffee/client\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 200
    end
  end
end

```

{% endcode %}

Algo digno de se notar, acabamos com a dependência do Tesla em nosso teste. Não somos mais dependentes dele, pelo menos aqui no teste e isso é incrível.

Mas se você rodar esse teste, ele vai quebrar.

```
mix test
```

<pre class="language-elixir"><code class="lang-elixir">> mix test test/integrations/coffee/client_test.exs


  1) test all_hot_coffees/0 respond a list of hot coffees (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:13
     match (=) failed
     code:  assert {:ok, %Response{}} = Client.all_hot_coffees()
<strong>     left:  {:ok, %CoffeeShop.Integrations.Coffee.Response{}}
</strong>     right: {
              :ok,
<strong>              %Tesla.Env{
</strong>                __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil},
                __module__: CoffeeShop.Integrations.Coffee.Client,
                body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&#x26;fit=crop&#x26;q=60&#x26;w=800&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&#x26;fit=crop&#x26;q=80&#x26;w=1887&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&#x26;fit=crop&#x26;q=60&#x26;w=800&#x26;ixlib=rb-4.0.3&#x26;ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" &#x3C;> ...,
                headers: [{"connection", "keep-alive"}, {"date", "Thu, 11 Apr 2024 19:37:53 GMT"}, {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""}, {"server", "cloudflare"}, {"content-length", "8671"}, {"content-type", "application/json; charset=utf-8"}, {"x-powered-by", "Express"}, {"access-control-allow-origin", "*"}, {"x-ratelimit-limit", "5000"}, {"x-ratelimit-remaining", "4991"}, {"x-ratelimit-reset", "1712864465"}, {"x-content-type-options", "nosniff"}, {"cf-cache-status", "DYNAMIC"}, {"report-to", "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=7uthBMHN%2FyIQOeGjes6brn7pVz5jkwb6u2f8wsWg5kqNYpwpgL4PmuMSqoptF3GfAcomcro%2BF1Xgb%2BgO7XyYtlwbnqvluFhvMqmfDXyVnzwo0IDw%2Fow23Ab%2B1ID8zcNWqb6Njpo%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, {"nel", "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, {"cf-ray", "872d6f0b1f2da415-GRU"}, {"alt-svc", "h3=\":443\"; ma=86400"}],
                method: :get,
                opts: [],
                query: [],
                status: 200,
                url: "https://api.sampleapis.com/coffee/hot"
              }
            }
     stacktrace:
       test/integrations/coffee/client_test.exs:14: (test)


Finished in 0.7 seconds (0.00s async, 0.7s sync)
1 test, 1 failure
</code></pre>

O problema é, estamos esperando um `%Response{}`, mas nosso cliente retornou um `%Tesla.Env{}`. Precisamos alterar nossa implementação em `client.ex`. Temos essa implementação:

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  plug(Tesla.Middleware.BaseUrl, "https://api.sampleapis.com")

  def all_hot_coffees do
<strong>    get("/coffee/hot")
</strong>  end
end

</code></pre>

O importante está na linha 7. A função `get/1` é do Tesla e sua resposta é um `Tesla.Env`. Estando na ultima linha da função é o que será retornado. Precisamos utilizar aqui nosso `Response.build/1` para retonrnar a estrutura esperada. Vamos fazer essa atualização e aproveitar para deixar as coisas mais bonitas usando pipe.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

<strong>  alias CoffeeShop.Integrations.Coffee.Response
</strong>
  plug(Tesla.Middleware.BaseUrl, "https://api.sampleapis.com")

  def all_hot_coffees do
<strong>    "/coffee/hot"
</strong><strong>    |> get()
</strong><strong>    |> Response.build()
</strong>  end
end

</code></pre>

Adicionamos nosso `Response.build/1` que espera um `{:ok, %Tesla.Env{}}` e tratamos a resposta para retornar um `%Response{}` tirando a dependência do `Tesla,` colocamos a responsabilidade para algo em nosso controle.

Rodaremos novamente o teste

```
mix test
```

```elixir
> mix test
....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 3 tests, 0 failures
```

## Conclusão

Conseguimos criar uma estrutura de resposta em nosso controle tirando a responsabilidade de uma biblioteca de terceiro. Isso pode parecer um tanto simples e exagerado, mas vai ser de grande ajuda. Tanto para debug quanto para leitura e entendimento.

Agora precisamos falar sobre estrategia de testes. Vamos lá.


# Estratégia de teste para requisições

Muitas vezes queremos forçar um cenário para ver se nossa aplicação lida bem com ela. Um grande variantes são erros:

* Serviço externo caiu e gerou erro **500**
* Serviço externo ficou lento e gerou um erro timeout com status **408**
* Serviço  externo não quer que você fique requisitando o tempo todo e gerou um **too much requests com status 428**
* Enviamos dados incorretos e o serviço não sabe lidar com eles gerando status **422**

São vários cenários e precisamos cobrir a maioria, principalmente quando lidamos com algo que não está em nosso controle que é o caso de requisições externas.

Mas como forçar esses erros? Duvido muito que o serviço externo queira disponibilizar uma rota de erro ou quebrar seu app só para nossos testes.  Isso também causaria problema de lentidao nos testes. Imagina, ter 10 testes de integração com o serviço externo e demorar 3 segundos para rodar cada um (é uma chamada externa, 3 segundos é bem rápido), teríamos um total de 30 segundos apenas para rodar esses testes. A cada teste novo, teríamos  um acréscimo na lentidão até um ponto que se tornaria inviável.

Também temos outro problema. Muitas vezes o serviço é pago por utilização.  Imagina rodar 1000 testes e ser cobrado por isso. Nem foi seu usuário que usou, foi você. Seria bem triste chegar no final do mês com uma conta alto de testes não?

Uma boa pratica é a utilização de ferramentas que fazem um *mock* de nossa requisição. Usamos a palavra *mock* como um adjetivo, isso é, Falso ou Simulado. Sendo assim, simulamos um chamada falsa. Visualmente, essa é a ideia

<figure><img src="/files/zsVuOF4MMHlq2guXfxCp" alt=""><figcaption><p>Imagem 01</p></figcaption></figure>

Em testes iremos evitar requisitar o serviço diretamente, mas iremos usar como base o retorno dele. Essa resposta será adicionado em nossa massa de teste e ela é imutável. Isso quer dizer, uma vez que o serviço mude a resposta, ele não refletirá nesse dado criado em nosso projeto. Teremos que alterar eles na mão. Mesmo com esse contra tempo, o custo é baixo, uma vez que não trocamos com tanta facilidade a resposta. Mas fiquem sempre atentos a isso.&#x20;

## Um pouco de explicação

Primeiro de tudo, vamos entender como nosso ciclo de vida funciona.

<figure><img src="/files/m1dZ2YSM3DpNbfaW92cx" alt=""><figcaption><p>Imagem 02</p></figcaption></figure>

1. O *Client* configura e mantem as funções relacionados ao serviço externo;
2. Dentro dele a função `all_hot_coffees/0` executa a requisição;
3. O *Tesla* roda a chamada utilizando a função do método *GET* e aguarda a resposta do serviço;
4. O serviço devolve a resposta fazendo o caminho de inverso;
5. Recebemos a resposta  e a tratamos como quisermos.

Entendido isso, vamos ver as formas de realizar esse desvio, para que não chegarmos a bater no serviço e termos uma resposta, como na imagem 01.&#x20;

Estamos utilizando Tesla como cliente *HTTP*. Na própria ferramenta existe [formas de criar esse *mock*](https://hexdocs.pm/tesla/readme.html#testing). A ideia por trás é fazer o *mock* diretamente na função *get* do Tesla, deixando nosso fluxo assim

<figure><img src="/files/1aG3wDT6n1ORfappws8n" alt=""><figcaption><p>Imagem 03</p></figcaption></figure>

1. O *Client* configura e mantem as funções relacionados ao serviço externo;
2. Dentro dele a função `all_hot_coffees/0` executa a requisição;
3. O *Tesla* entrega diretamente a resposta falsa para o método *GET*;
4. Recebemos a resposta e a tratamos como quisermos.

Isso já resolve nosso problema. Não ficamos batendo todo tempo no serviço e podemos simular diversos cenários.

Porém, um ponto de atenção. Percebeu que tem um passo a menos quando o mock está ativo? Isso acontece porque não temos um requisição real quando *mockamos* com o Tesla. A requisição nunca é feito de verdade. Particularmente eu não gosto disso. Quando realizamos requisições, algumas coisas devem ser garantidas. Como formato de envio, resposta criado pela ferramentas entre o outros pontos, mas no *Tesla.Mock* criamos isso tudo na mão e a chance de algo incoerente acontecer não é pequena. Pode ser até a atualização da biblioteca ou utilização interna.

Eu prefiro trabalhar com requisições reais. Nessa hora você deve estar com vontade de enfiar esse livro no meu... Calma! Falei que prefiro que a requisição seja real, não que o dado seja real.

O fluxo que eu sempre desejo seria assim:

<figure><img src="/files/z7PkDNbWZxPR7EegbP8b" alt=""><figcaption><p>Imagem 04</p></figcaption></figure>

1. O *Client* configura e mantem as funções relacionados ao serviço externo;
2. Dentro dele a função `all_hot_coffees/0` executa a requisição;
3. O *Tesla* roda a chamada utilizando a função do método *GET* e aguarda a resposta do serviço;
4. O serviço devolve a resposta *mockada* fazendo o caminho de inverso;
5. Recebemos a resposta  e a tratamos como quisermos.

Você já deve ter notado que é basicamente o mesmo passo a passo da requisição real, mas, ao invés de bater no serviço, temos um serviço de teste. Gosto bastante desse modelo porque temos um real entendimento do que esta acontecendo. Uma requisição *HTTP* é feito para um "serviço" false que temos disponível e que responde igual a uma resposta HTTP. Isso é perfeito para garantir comportamento real x teste.

Para resolver isso, existe uma biblioteca chamada [*Bypass* ](https://hexdocs.pm/bypass/Bypass.html)que gosto bastante. Utilizaremos ela.


# Instalando Bypass

Como qualquer outra biblioteca elixir, *bypass* é facilmente instalado. Basta adicionar ele no mix.exs e rodar o comando de dependências

<pre class="language-elixir" data-title="mix.exs" data-line-numbers><code class="lang-elixir">defp deps do
  [
    {:tesla, "~> 1.4"},
<strong>    {:bypass, "~> 2.1"}
</strong>  ]
end
</code></pre>

```
mix deps.get
```

{% code fullWidth="false" %}

```sh
> mix deps.get                                     
Resolving Hex dependencies...
Resolution completed in 0.22s
New:
  bypass 2.1.0
  cowboy 2.12.0
  cowboy_telemetry 0.4.0
  cowlib 2.13.0
  plug 1.15.3
  plug_cowboy 2.7.1
  plug_crypto 2.0.0
  ranch 1.8.0
  telemetry 1.2.1
Unchanged:
  mime 2.0.5
  tesla 1.8.0
* Getting bypass (Hex package)
* Getting plug (Hex package)
* Getting plug_cowboy (Hex package)
* Getting ranch (Hex package)
* Getting cowboy (Hex package)
* Getting cowboy_telemetry (Hex package)
* Getting telemetry (Hex package)
* Getting cowlib (Hex package)
* Getting plug_crypto (Hex package)
You have added/upgraded packages you could sponsor, run `mix hex.sponsor` to learn more
```

{% endcode %}

Algumas outras bibliotecas vieram junto para conseguir lidar com requisições HTTP. Mas não vamos nos preocupar com isso.

A ideia é cada vez que rodarmos testes que precisam fazer requisições externas, tenhamos um serviço pronto para receber e trazer para nós a resposta que esperamos. Igual um serviço como nossa aplicação.

Vamos ver como fazer isso a seguir.


# Mockando requisições do cliente com Bypass

Primeiro vamos criar o teste que precisamos para quando o serviço está quebrado. Um erro 500 critico. Vamos adicionar o  novo teste em nosso arquivo `test/integrations/coffee/client_test.exs`&#x20;

{% code title="test/integrations/coffee/client\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client

  describe "all_hot_coffees/0" do
    # ...
    
    test "service is crashed" do
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 500
    end
  end
end
```

{% endcode %}

Em nosso teste queremos que o status seja 500. Mas ainda estamos batendo no serviço real. Precisamos configurar o bypass e utilizar em nosso teste, para conseguir simular as respostas que queremos.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
<strong>    setup do
</strong><strong>      bypass = Bypass.open(port: 3000)
</strong><strong>      {:ok, bypass: bypass}
</strong><strong>    end
</strong>    
    # ...

<strong>    test "service is crashed", %{bypass: bypass} do
</strong>      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 500
    end
  end
end

</code></pre>

Adicionamos na função setup o `Bypass.open/1`, que tem como objetivo abrir um serviço falso. Esse serviço é levantado em `http//localhost` podendo ser adicionado a porta em nosso caso, escolhemos a porta 3000. Isso pode conflitar com outros serviços em seu computador, então, caso de problema, troque a porta.

Com o serviço de pé, precisamos ajeitar nosso mock. Para criar o mock, precisamos primeiro identificar qual a requisição queremos mockar. Para isso o *Bypass* possui o método `expect_once` e `expect` . Um espera apenas uma chamada, o outro uma ou mais.&#x20;

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    setup do
      bypass = Bypass.open()
      {:ok, bypass: bypass}
    end

    test "respond a list of hot coffees", %{bypass: bypass} do
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 200
    end

    test "service is crashed", %{bypass: bypass} do
<strong>      response = "Server exploded"
</strong>      
<strong>      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
</strong><strong>        Plug.Conn.resp(conn, 500, response)
</strong><strong>      end)
</strong>
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 500
    end
  end
end

</code></pre>

Chamamos a função `Bypass.expect_once/4`, passando a instância do serviço, o verbo que utilizamos, o recurso e a função de *callback*, que nada mais é que a resposta da requisição. *Plug* montara uma resposta HTTP para nós. É aqui que a informação falsa deve aparecer. Feito isso, vamos rodar nosso teste

```
mix test
```

```sh
> mix test 
.

  1) test all_hot_coffees/0 service is crashed (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:18
     Assertion with == failed
     code:  assert status == 500
     left:  200
     right: 500
     stacktrace:
       test/integrations/coffee/client_test.exs:26: (test)

...
Finished in 0.8 seconds (0.00s async, 0.8s sync)
1 doctest, 4 tests, 1 failure
```

O teste falhou. Ainda temos um status 200. Isso quer dizer que estamos batendo no serviço real. Com toda certeza, ainda estamos indo para a URL base aplicado pelo plug e o novo serviço mora em `http://localhost` e precisamos bater la para fins de teste.

Com isso, temos que achar uma forma de poder reconfigurar a URL do nosso cliente quando estamos no teste. Existe a possiblidade de configurar pelas variáveis de ambiente, adicionando um para `config/config.exs` e um para `config/test.exs`. Porém, sou bem visual e usar isso:

```elixir
plug(Tesla.Middleware.BaseUrl, Application.get_env(:coffee_shope, :coffee)[:base_url])
```

Ao invés disso:

```elixir
plug(Tesla.Middleware.BaseUrl, "https://api.sampleapis.com")
```

Me da dor de cabeça.

A opção que gosto é configurar com base do parâmetro `opts` passando para o cliente.&#x20;

```elixir
Client.all_hot_coffees(base_url: "http://localhost")
```

Não só a *url* *base* mas todo tipo de configuração gosto de passar pelos parâmetros. Isso facilita a configuração e a utilização para os testes.

O problema agora é que utilizamos o *plug* para configurar e não conseguimos alcançar ele dessa forma. Precisamos mudar a forma como nosso cliente esta rodando.&#x20;

* Remover plug de configuração
* Passar *url* base para função

Vamos começar removendo o plug para evitar  as configurações via macro.

{% code title="lib/integrations/coffee/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees do
    "/coffee/hot"
    |> get()
    |> Response.build()
  end
end

```

{% endcode %}

Podemos rodar os testes e ver o tamanho do estrago.

```
mix test
```

```
mix test 
Compiling 1 file (.ex)


  1) test all_hot_coffees/0 respond a list of hot coffees (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:13
     ** (FunctionClauseError) no function clause matching in CoffeeShop.Integrations.Coffee.Response.build/1

     The following arguments were given to CoffeeShop.Integrations.Coffee.Response.build/1:
     
         # 1
         {:error, {:no_scheme}}
     
     Attempted function clauses (showing 1 out of 1):
     
         def build({:ok, %Tesla.Env{status: status, body: body}})
     
     code: assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
     stacktrace:
       (coffee_shop 0.1.0) lib/integrations/coffee/response.ex:4: CoffeeShop.Integrations.Coffee.Response.build/1
       test/integrations/coffee/client_test.exs:14: (test)



  2) test all_hot_coffees/0 service is crashed (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:18
     ** (FunctionClauseError) no function clause matching in CoffeeShop.Integrations.Coffee.Response.build/1

     The following arguments were given to CoffeeShop.Integrations.Coffee.Response.build/1:
     
         # 1
         {:error, {:no_scheme}}
     
     Attempted function clauses (showing 1 out of 1):
     
         def build({:ok, %Tesla.Env{status: status, body: body}})
                                                                                                                                                                                                                                                                                                                                                     M:   CPU: 
     code: assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
     stacktrace:
       (coffee_shop 0.1.0) lib/integrations/coffee/response.ex:4: CoffeeShop.Integrations.Coffee.Response.build/1
       test/integrations/coffee/client_test.exs:25: (test)

...
Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 doctest, 4 tests, 2 failures
```

A vantagem de ter testes, é que saberemos se algo da errado em nossa mudança de forma prática e rápida.  Caso quebre, basta resolver, ficando verde, temos um indicativo que tudo voltou a funcionar. Vamos fazer essas belezinhas passarem.

O problema aqui é não ter configurado a *url base* do cliente, uma vez que removemos o plug. Precisamos ir agora para a segunda parte, montar nossa função de criação do cliente.

* ~~Remover plug de configuração~~
* Passar *url* base para função

Precisamos passar um parâmetro de configuração para dentro de nossa função de requisição `all_hot_coffees/1`. Utilizaremos uma lista para facilitar a adição de novos argumentos.  Utilizaremos `Keyword.get/3` para obter o dado da lista:

**Keyword.get/3**

1. **Primeiro argumento**: Lista
2. **Segundo argumento:** chave da lista
3. **Terceiro argumento:** Valor padrão caso não encontre a chave

Funcionará assim:

<pre class="language-elixir" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  alias CoffeeShop.Integrations.Coffee.Response

<strong>  def all_hot_coffees(opts \\ []) do
</strong><strong>    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")
</strong>
<strong>    "#{base_url}/coffee/hot"
</strong>    |> get()
    |> Response.build()
  end
end

</code></pre>

Passamos o parâmetros de `opts` para nossa função e dissemos que caso não encontre em `opts` a chave `base_url`, utilizar o valor `https://api.sampleapis.com`. Nossa implementação continuará funcionando com a url base padrão, mas temos a possibilidade de alterar caso necessario, em nosso atual cenário, quando precisar bater no serviço de teste.&#x20;

Mas isso não é tudo que precisamos fazer. Se rodar o teste estaremos ainda com o problema de sempre acessar o serviço real. Para resolver isso, precisamos passar `base_url` nos parâmetros da chamada em nosso teste como esperado.

{% code title="test/integrations/coffee/client\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    setup do
      bypass = Bypass.open(port: 3000)
      {:ok, bypass: bypass}
    end

    test "respond a list of hot coffees" do
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 200
    end

    test "service is crashed", %{bypass: bypass} do
      response = "Server explode"

      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
        Plug.Conn.resp(conn, 500, response)
      end)
      
      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 500
    end
  end
end

```

{% endcode %}

Rodaresmos o teste

```
mix test
```

```
> mix test
.....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 4 tests, 0 failures

Randomized with seed 431636
```

Nada mal em? Criamos um mecanismo simples de configuração de nosso cliente. Podendo adicionar mais opções, que vamos explorar mais a frente. Por hora, conseguimos criar chamadas falsas do jeito que quisermos.

Tem duas coisas me incomodando:

1. Estamos usando uma macro do Tesla que não tras muito ganho para nós.
2. Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso

Vamos começar removendo a macro. A macro do Tesla nos da poder de usar algumas funções sem precisar utilizar o prefixo, como se pertencesse a esse modulo. Mas além de criar um acoplamento forte também não fica muito legal visualmente.&#x20;

<pre class="language-elixir"><code class="lang-elixir">"#{base_url}/coffee/hot"
<strong>|> get()
</strong>|> Response.build()
</code></pre>

Podemos chamar a mesma função usando `Tesla.get/1`, não precisando da macro.

{% code title="lib/integrations/coffee/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    "#{base_url}/coffee/hot"
    |> Tesla.get()
    |> Response.build()
  end
end

```

{% endcode %}

Sem mais macros. Sem mais magia obscura.&#x20;

1. ~~Estamos usando uma macro do Tesla que não trás muito ganho para nós.~~
2. Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso

Vamos atualizar nosso teste para que use o dado mockado e pare de realizar a requisição para o serviço real.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    setup do
      bypass = Bypass.open(port: 3000)
      {:ok, bypass: bypass}
    end

    test "respond a list of hot coffees", %{bypass: bypass} do
<strong>      response = ""
</strong>
<strong>      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
</strong><strong>        Plug.Conn.resp(conn, 200, response)
</strong><strong>      end)
</strong>
<strong>      opts = [
</strong><strong>        base_url: "http://localhost:3000"
</strong><strong>      ]
</strong>
<strong>      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
</strong>      assert status == 200
    end

    # ...
  end
end

</code></pre>

Uma vez o mecanismo pronto, basta apenas definir o *mock* e configurar a *url* base  para nosso serviço falso.

* ~~Estamos usando uma macro do Tesla que não trás muito ganho para nós.~~
* ~~Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso~~

Estamos 100% independentes agora. Não irão mais nos cobrar por requisições de teste. Também conseguimos limpar nosso cliente e remover todas os acoplamentos e dependências que não queremos.

Vamos mais a frente, temos muito chão ainda.


# Tratando dados da resposta

Você deve ter notado que a resposta do cliente so testa o status.&#x20;

<pre class="language-elixir" data-title="" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  # ...
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees", %{bypass: bypass} do
      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
        Plug.Conn.resp(conn, 200, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

<strong>      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
</strong><strong>      assert status == 200
</strong>    end
    
    # ...
  end
end
</code></pre>

Não conseguimos fazer muita coisa apenas com o status. Precisamos de dados.&#x20;

Para deixar as coisas mais estruturadas, utilizarei nesse estudo utilizarei struct onde conseguir tirar mais proveito. Você pode não precisar de tanta estrutura, isso vai depender do seu projeto.&#x20;

Seguimos.&#x20;

Agora precisamos obter uma resposta de nosso serviço, para usar como dado. Acessando o endpoint, conseguimos isso fácil: <https://api.sampleapis.com/coffee/hot>

Com o dado em mão, criamos o  arquivo e adicionamos a resposta diretamente em JSON.

{% code title="test/coffee\_shop/integrations/coffee/fixtures/success.json" lineNumbers="true" %}

```json
[{"title":"Caramel Latte","description":"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.","ingredients":["Espresso","Ångad mjölk","Karamellsirap"],"image":"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":3},{"title":"Cappuccino","description":"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.","ingredients":["Espresso","Ångad mjölk","Foam"],"image":"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":4},{"title":"Americano","description":"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.","ingredients":["Espresso","Hett vatten"],"image":"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":5},{"title":"Espresso","description":"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.","ingredients":["Espresso"],"image":"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":6},{"title":"Macchiato","description":"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.","ingredients":["Espresso","Foam"],"image":"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":7},{"title":"Mocha","description":"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.","ingredients":["Espresso","Ångad mjölk","Choklad"],"image":"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D","id":8},{"title":"Hot Chocolate","description":"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.","ingredients":["Choklad","Mjölk"],"image":"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D","id":9},{"title":"Chai Latte","description":"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.","ingredients":["Te","Mjölk","Ingefära","Kardemumma","Kanel"],"image":"https://images.unsplash.com/photo-1578899952107-9c390f1af1b7?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGNoYWklMjBsYXR0ZXxlbnwwfHwwfHx8MA%3D%3D","id":10},{"title":"Matcha Latte","description":"Matcha latte är en grön, hälsosam kaffedryck med finkrossad matcha-te och mjölk, erbjuder mild sötma, en unik smak och en mild koffeinkick.","ingredients":["Matcha-pulver","Mjölk","Socker*"],"image":"https://images.unsplash.com/photo-1536256263959-770b48d82b0a?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8bWF0Y2hhJTIwbGF0dGV8ZW58MHx8MHx8fDA%3D","id":11},{"title":"Seasonal Brew","description":"Säsongs kaffe med olika smaktoner som karamell, frukt och choklad","ingredients":["Kaffe"],"image":"https://images.unsplash.com/photo-1611162458324-aae1eb4129a4?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTg1fHxibGFjayUyMGNvZmZlZXxlbnwwfHwwfHx8MA%3D%3D","id":12},{"title":"Svart Te","description":"Svart te föddes i Kina. Det är tillverkat av blad från en växt som kallas Camellia och kan smaksättas olika med frukter till exempel. En trevlig, varm, smakfull och aromatisk dryck som passar till vardagen.","ingredients":["Te"],"image":"https://images.unsplash.com/photo-1576092768241-dec231879fc3?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjB8fHRlYXxlbnwwfHwwfHx8MA%3D%3D","id":13},{"title":"Islatte","description":"Iced latte är en kyld kaffedryck som görs genom att blanda espresso och kyld mjölk. Den serveras med isbitar och är även känd som cafè latte iced eller latte on the rocks.","ingredients":["Espresso","Mjölk","Is","Sirap"],"image":"https://images.unsplash.com/photo-1517701550927-30cf4ba1dba5?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8aWNlZCUyMGxhdHRlfGVufDB8fDB8fHww","id":14},{"title":"Islatte Mocha","description":"Iced latte Mocha är en kombination av latte och mocha, som i sig är en kombination av choklad och kaffe. Den ger kalla dryckälskare en läcker upplevelse av choklad och kaffe.","ingredients":["Espresso","Is","Mjölk","Choklad "],"image":"https://images.unsplash.com/photo-1642647391072-6a2416f048e5?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mzh8fGljZWQlMjBtb2NoYSUyMGxhdHRlfGVufDB8fDB8fHww","id":15},{"title":"Frapino Caramel","description":"Det är en blandad eller bättre sagt skakad kaffe med vispad grädde på toppen. Ett måste för varma sommardagar.","ingredients":["coffee","Is","Mjölk","Karamellsirap","Vispgrädde*","Karamellsås"],"image":"https://images.unsplash.com/photo-1662047102608-a6f2e492411f?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8ZnJhcGlubyUyMGNhcmFtZWx8ZW58MHx8MHx8fDA%3D","id":16},{"title":"Frapino Mocka","description":"Ännu en berömd och utsökt kall dryck för dem som föredrar choklad. Tänk dig smaken av en shake med choklad och vispad grädde på toppen.","ingredients":["Coffee","Is","Mjölk","Cocoa","Vispgrädde*"],"image":"https://images.unsplash.com/photo-1530373239216-42518e6b4063?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8ZnJhcGlubyUyMG1vY2hhfGVufDB8fDB8fHww","id":17},{"title":"Apelsinjuice","description":"Vi har inget att säga om vår nypressade apelsinjuice. Du måste prova den själv.","ingredients":["Färska Apelsiner","Is"],"image":"https://images.unsplash.com/photo-1600271886742-f049cd451bba?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NzF8fG9yYW5nZSUyMGp1aWNlfGVufDB8fDB8fHww","id":18}]
```

{% endcode %}

{% hint style="warning" %}
Optei por deixar o dado dentro do contexto. Assim facilita a nomeação e utilização. Junto com entendimento e leitura.
{% endhint %}

Depois precisamos carregar esse arquivo em nosso teste e retornar a resposta dele em nosso mock.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Coffee.Client

  describe "all_hot_coffees/0" do
    # ...
    test "respond a list of hot coffees", %{bypass: bypass} do
<strong>      response =
</strong><strong>        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")
</strong>
      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        Plug.Conn.resp(conn, 200, response)
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

<strong>      assert {:ok, %Response{status: status, body: body}} = Client.all_hot_coffees(opts)
</strong>      
      assert status == 200
    end
    
    # ...
  end
end
</code></pre>

Mockamos uma resposta usando o arquivo e atualizamos nossas assertions para garantir que algo está vindo de resposta no body.&#x20;

```
mix test
```

```
> mix test
.....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 4 tests, 0 failures
```

Perfeito, temos um *body* com uma resposta no formato de string e uma estrutura JSON dentro. Para verificarmos se esta tudo certo, podemos utilizar o iex.

```
iex -S mix
```

Obtendo dados:

```elixir
{:ok, %CoffeeShop.Integrations.Coffee.Response{body: body}} = CoffeeShop.Integrations.Coffee.Client.all_hot_coffees()
```

```elixir
{:ok, 
   %CoffeeShop.Integrations.Coffee.Response{
     status: 200,
     body: "[{\"title\": \"xxxxx\", \"description\": \"zzzz\"}]"
   }
}
```

Pode notar que o body vem com scapes, que são essas barras antes de cada aspas duplas (\\") deixando utilizar aspas duplas dentro de aspas duplas.&#x20;

Como pode ser notado, temos os dados em mãos, porém temos a dificuldade de pegar um dado especifico, uma vez que ele não está identificado como estrutura de fácil acesso. Para facilitar isso, podemos decodificar o JSON transformando o dado para estruturas que o elixir consegue lidar melhor, lista e mapas.

O responsável por lidar com a resposta é nossa função `Response.build/1`. Vamos atualizar seu test para refletir o que queremos. Para termos um cenário mais próximo do real, podemos usar o arquivo JSON que criamos.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/response_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    test "build from Tesla.Env structure" do
<strong>      response =
</strong><strong>        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")
</strong>
      tesla_response = {:ok, %Tesla.Env{status: 200, body: response}}

      assert {:ok, %Response{body: body}} = Response.build(tesla_response)
<strong>      assert is_list(body)
</strong>    end
  end
end
</code></pre>

Rodaremos o teste

```
mix test
```

```
 1) test build/1 build from Tesla.Env structure (CoffeeShop.Integrations.Coffee.ResponseTest)
     test/coffee_shop/integrations/coffee/response_test.exs:7
     Expected truthy, got false
     code: assert is_list(body)
     arguments:

         # 1
         "[{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan kar
amell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämighe
ten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"im
age\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixl
ib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino
\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö a
v kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjö
lk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"]
,\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&i
xlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"American
o\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt 
med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com
/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG
90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskot
t kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"i
ngredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto
=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\
"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har
 en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ing
redients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?
auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli för
älskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espr
esso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444
ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3
D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm 
choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energi
givande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-
1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8f
GhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du
 letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kane
l ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"imag
e\":\"https://images.unsplash.com/photo-1578899952107-9c390f1af1b7?w=900&auto=format&fit=crop&q=60&ixlib=
rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGNoYWklMjBsYXR0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":10},{\"title\
":\"Matcha Latte\",\"description\":\"Matcha latte är en grön, hälsosam kaffedryck med finkrossad matcha-t
e och mjölk, erbjuder mild sötma, en unik smak och en mild koffeinkick.\",\"ingredients\":[\"Matcha-pulve
r\",\"Mjölk\",\"Socker*\"],\"image\":\"https://images.unsplash.com/photo-1536256263959-770b48d82b0a?w=900
&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8bWF0Y2hhJTIwbGF0dGV8ZW58MHx8MH
x8fDA%3D\",\"id\":11},{\"title\":\"Seasonal Brew\",\"description\":\"Säsongs kaffe med olika smaktoner so
m karamell, frukt och choklad\",\"ingredients\":[\"Kaffe\"],\"image\":\"https://images.unsplash.com/photo
-1611162458324-aae1eb4129a4?w=900&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M
Tg1fHxib" <> ...

     stacktrace:
       test/coffee_shop/integrations/coffee/response_test.exs:14: (test)


Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 test, 1 failure

```

Falhou como esperado, devido a não ser uma lista. Agora precisamos resolver isso na função `Response.build/1`.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/response_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map

  def build({:ok, %Tesla.Env{status: status, body: body}}) do
    response = %__MODULE__{
      status: status,
<strong>      body: JSON.decode!(body)
</strong>    }

    {:ok, response}
  end
end
</code></pre>

Utilizamos o `JSON.decode!/1` para decodificar o JSON de nossa resposta.

```
mix test test/coffee_shop/integrations/coffee/response_test.exs
```

```
.
Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 test, 0 failures
```

Com o dado vindo no formato que o Elixir consegue tratar, podemos validar sua estrutura:

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/response_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    test "build from Tesla.Env structure" do
      response =
        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")

      tesla_response = {:ok, %Tesla.Env{status: 200, body: response}}

      assert {:ok, %Response{body: body}} = Response.build(tesla_response)
      assert is_list(body)

<strong>      assert %{
</strong><strong>        "title" => _,
</strong><strong>        "description" => _,
</strong><strong>        "id" => _,
</strong><strong>        "image" => _
</strong><strong>       } = List.first(body)
</strong>    end
  end
end
</code></pre>

```
mix test test/coffee_shop/integrations/coffee/response_test.exs
```

```
.
Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 test, 0 failures
```

Nosso response está funcionando e pronto para uso. Vamos ver se tudo continua como deve estar.

```
mix test
```

```
> mix test
.....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 4 tests, 0 failures
```

Tudo em seu devido lugar.


# Erro genérico

Quando nos conectamos a um serviço externo, abrimos uma brecha em nossas defesas. Podemos cair em cenários que não esperamos, como algum erro não mapeado ou até mesmo, o serviço parar de responder. Quando isso acontece, precisamos avisar nosso usuário elegantemente que algo deu errado.&#x20;

Vamos a um exemplo prático. Nosso tratamento esta assim

{% code title="lib/coffee\_shop/integrations/coffee/response.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: %{}

  def build({:ok, %Tesla.Env{status: status, body: body}}) do
    response = %__MODULE__{
      status: status,
      body: JSON.decode!(body)
    }

    {:ok, response}
  end
end
```

{% endcode %}

Agora, vamos pensar. Caso algo de errado, ele vai entrar em nosso `build/1`, e fazer o `JSON.decode!/1`. Mas, e se o body, quando der erro, vir em um formato diferente? O que vai acontecer. Vamos atualizar nosso teste:

{% code title="test/coffee\_shop/integrations/coffee/response\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    # ...

    test "build an 500 error" do
      tesla_response = {:ok, %Tesla.Env{status: 500, body: "Server exploded"}}
      
      assert {:ok, %Response{body: _body}} = Response.build(tesla_response)
    end
  end
end
```

{% endcode %}

Criamos um teste simples com uma resposta em formato fora do padrão. Vamos rodar esse teste.

```
mix test 
```

<pre class="language-elixir"><code class="lang-elixir">> mix test 
1) test build/1 build an 500 error (CoffeeShop.Integrations.Coffee.ResponseTest)
     test/coffee_shop/integrations/coffee/response_test.exs:19
<strong>     ** (JSON.Decoder.UnexpectedTokenError) Invalid JSON - unexpected token >>Server exploded&#x3C;&#x3C;
</strong>     code: assert {:ok, %Response{body: _body}} = Response.build(tesla_response)
     stacktrace:
       (json 1.4.1) lib/json.ex:83: JSON.decode!/1
       (coffee_shop 0.1.0) lib/coffee_shop/integrations/coffee/response.ex:7: CoffeeShop.Integrations.Cof
fee.Response.build/1
       test/coffee_shop/integrations/coffee/response_test.exs:24: (test)


Finished in 0.05 seconds (0.00s async, 0.05s sync)
2 tests, 1 failure, 1 excluded

</code></pre>

Recebemos um erro de `unexpected token`. Isso ocorre porque o JSON.decode!/1 nao conseguir fazer o parse de "Server exploded". Vamos tentar fazer na mao

```
iex -S mix
```

```elixir
JSON.decode!("Server exploded")
```

```elixir
** (JSON.Decoder.UnexpectedTokenError) Invalid JSON - unexpected token >>Server exploded<<
```

Precisamos de uma estrutura em JSON, mas nosso serviço não trás esse formato para nós. Temos várias formas de lidar com isso. A mais simples é responder um erro genérico quando receber um status 500. Ou melhor, quando não receber o status esperado. Isso quer dizer, tudo o que não for esperado, vai cair no erro genérico, apenas para não gerar um erro incompreenssivel ou feio para o usuário.

{% hint style="info" %}
Aqui é um bom local para mapearmos erros não conhecidos e ai sim, implementar uma solução para eles.&#x20;
{% endhint %}

Entendido isso, vamos melhorar nosso teste. Nele precisamos saber que&#x20;

* Um erro ocorreu;
* &#x20;Mensagem para avisarmos o usuário.

{% code title="test/coffee\_shop/integrations/coffee/response\_test.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    # ...

    test "build an 500 error" do
      tesla_response = {:ok, %Tesla.Env{status: 500, body: "Server exploded"}}
      
      assert {:error, %Response{body: _body, error: error}} = Response.build(tesla_response)
      assert error == "Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"
    end
  end
end
```

{% endcode %}

No teste alteramos a assertion da resposta, onde esperamos agora um `{:error, %Response{}}` para refletir que um erro aconteceu. Tambem adicionamos o error sendo a mensagem que ele retorna.

Vamos em nosso `response.ex` atualizar a construção de nossa resposta

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/response.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map

<strong>  @success_status [200]
</strong>
  def build({:ok, %Tesla.Env{status: status, body: body}})
<strong>      when status in @success_status do
</strong>    response = %__MODULE__{
      status: status,
      body: JSON.decode!(body)
    }

    {:ok, response}
  end
end
</code></pre>

Adicionamos o *status* que podem realizar o `build/1`. Isso quer dizer, caso um *status* nao esteja no `@success_status`, ele não entrará na função. &#x20;

Feito isso, precisamos criar uma função de mesmo nome para capturar todos os cenários restantes.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/response.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map, error: :string

<strong>  @success_status [200]
</strong>
  def build({:ok, %Tesla.Env{status: status, body: body}})
<strong>      when status in @success_status do
</strong>    response = %__MODULE__{
      status: status,
      body: JSON.decode!(body)
    }

    {:ok, response}
  end

<strong>  def build({:ok, %Tesla.Env{status: status, body: body}}) do
</strong><strong>    message = "Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"
</strong><strong>
</strong><strong>    response = %__MODULE__{
</strong><strong>      status: status,
</strong><strong>      error: message
</strong><strong>    }
</strong><strong>
</strong><strong>    {:error, response}
</strong><strong>  end
</strong>end
</code></pre>

Adicionamos um *guard clause* na primeira função, liberando apenas para `status 200`. Todo o restante cairá na segunda função e irá gerar um erro genérico da integração. Tabém adicionamos uma mensagem padrão e retornamos ao invés de `:ok`, um `:error` para refletir que algo deu errado.

```
mix test test/coffee_shop/integrations/coffee/response_test.exs
```

```elixir
mix test test/coffee_shop/integrations/coffee/response_test.exs
..
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 4 tests, 0 failures
```

Criamos um mecanismo simples de controle de status, onde podemos gerar N cenários para a resposta que vem do nosso serviço e tratar da melhor forma que quisermos.

{% hint style="warning" %}
Podemos criar outras funções com outras guard clauses para isolar melhor os tipos de resposta.
{% endhint %}

Ao rodar todos os testes, teremos uma quebra no client\_test.exs, isso devido a resposta mudar para uma tupla com :error. Basta altera-la e tudo volta a funciona.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Coffee.Client

  describe "all_hot_coffees/0" do
    # ...
    test "service is crashed", %{bypass: bypass} do
      response = %{message: "Server exploded"} |> JSON.encode!()

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
        Plug.Conn.resp(conn, 500, response)
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

<strong>      assert {:error, %Response{status: status, body: _body}} = Client.all_hot_coffees(opts)
</strong>      assert status == 500
    end
  end
end
</code></pre>

Feito. Agora temos outro problema para lidar.&#x20;


# O que é o rate limit

### Uma pequena história

Imagine que você criou um serviço. Outros programas se conectam a ele. Seu custo é baixo e isso é ótimo. De repente, algo explode em seu painel da AWS. A conta de $100 dolares vai para $500 sem a adição de nenhum cliente. Você não entende o que aconteceu. Acessa os logs e percebe que um único usuário (programa que se integra com você) está realizando 100 requisições por segundo. Isso não faz sentido, ele precisaria apenas de uma requisição a cada minuto para se manter atualizado. Para resolver esse problema, você pede para seu time implementar um *rate limit*.&#x20;

O usuário então tem permissão de fazer 5 requisições por minuto. Caso passe do limite, ele é penalizado e recebe um status indicando  que fez muitas requisições (status 429). Até que a penalidade seja resetada, o usuário estará bloqueado de requisitar recursos do serviço.

Com esse exemplo, demonstrei a necessidade do rate limit em serviços. Estou mostrando dessa forma para você entender que não é algo que existe apenas por existir. Ele tem um proposito de controle. Seja de recurso, seja de modelo de negócio (você pode pagar mais para liberar mais requisições se quiser.)

Já que você quer se conectar a outros serviços, acabará encontrando essa tecnica uma hora ou outra e vai precisar saber o que fazer em relação a ela.

### Mas o que é Rate Limit exatamente?

Ele é uma técnica para para limitar o número de requisições que podem ser feitos em uma faixa de tempo. Exemplo

* Máximo de 10 requisições por segundo
* Máximo de 500 requisições por minuto
* Máximo de 1000 requisições por mês

Ao chegar ao limite de execução, receberemos o [status 429](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Status/429).

**A decisão de APIs usarem ela são diversas:**

* Prevenir carga alta de requisições;
* Prevenir algum tipo de abuso em relação a API (principalmente públicas)
* Estratégia de negócio. Podendo enquadrar seu cliente em uma faixa de preço com um máximo de requisições.

Isso pode ser um problema quando tentamos integrar com algum serviço. Queremos nos beneficiar o máximo e não fazer requisições que desperdiçam a cota. Vamos dar uma olhada em possíveis soluções para esse limitador.


# Rate Limite de curta duração


# Reexecutando uma requisição

Alguns limitadores podem levar apenas alguns segundos para liberar, como por exemplo.

* Máximo de 3 requisições por segundo

Isso quer dizer que ao passar o segundo, teremos mais 10 requisições para fazer. Em termos de tempo, faz sentido aguardar o bloqueio. Adicionar esse tempo a requisição não impacta de forma tão negativa quanto receber uma mensagem de erro.&#x20;

Vamos primeiro criar nosso cenário de erro.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...

    test "recovery of too much requests", %{bypass: bypass} do
      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        Plug.Conn.resp(conn, 429, "")
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

<strong>      assert status == 200
</strong>    end
  end
end

</code></pre>

A ideia desse teste é conseguir se recuperar de um 429 utilizando a reexecução da requisição, esperando 1 segundos para acabar a penalidade. Obviamente, se rodar esse teste, receberemos um status 429.&#x20;

Para resolver esse problema, iremos utilizar um middleware do tesla, chamado [Tesla.Middleware.Retry](https://hexdocs.pm/tesla/Tesla.Middleware.Retry.html) onde passaremos os seguintes argumentos:

* ***delay*** -> Tempo de espera até próxima tentativa
* ***max\_retries*** -> Máximo de tentativas até retornar um erro
* ***max\_delay*** -> Máximo de tempo de espera
* ***should\_retry*** -> regra condicional para definir se uma requisição deve ou não reexecutar

Vamos lá. Não estamos mais usando a macro do Tesla, isso quer dizer, que não conseguiremos utilizar tiretamente o plug. Isso é bom, pode parecer mais trabalhoso, mas é bom. Para isso, precisamos definir os middlewares diretamente no [*Tesla.Client*](https://hexdocs.pm/tesla/Tesla.Client.html) passando os middlewares, que queremos. Até agora utilizamos o `Tesla.get/1`, passando por parâmetro o caminho completo de nosso *endpoint*. Porém, existe a função `Tesla.get/2`, onde o primeiro argumento é o `Client` e o segundo o *endpoint*. precisamos do client para configurar o middleware, usaremos ele.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

<strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: 1000,
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 2_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> false
</strong><strong>       end}
</strong><strong>    ]
</strong>
    middlewares
<strong>    |> Tesla.client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Conseguimos adicionar o middleware, mesmo tendo ficado meio cheio a função. Resolveremos isso depois. Agora você pode rodar seu teste.

Algo que devemos notar é a configuração should\_retry. É onde criamos a regra para rodar o *retry*. Ali temos a regra

* `{:ok, _} -> false` # Se nossa resposta for um :ok ele não deve rodar o retry
* `{:error, _} -> false` # Se for um :error não deve rodar (esse erro vem do Tesla, conexão estabelecida por alguma razão, seria um bom ponto para retry, mas não estamos vendo isso agora
* `{:ok, %{status: status}} when status in [429] -> true` # Esse estava no topo, mas coloquei por ultimo aqui para explicar melhor

Pegamos por pattern matching o status vindo do Tesla. Verificamos se esse status está dentro da lista ao fazer `status in [429]` caso esteja, queremos que faça o retry. Esta dentro de uma lista devido a poder colocar mais status ali. Um erro 500 seria legal, assim, alguns erros podem ser sanados sem nem transparecer para o usuário. Porem, estou me atentando apenas ao nosso inimigo 429.

Vamos rodar o teste.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

<pre class="language-elixir"><code class="lang-elixir">> mix test
15:35:22.169 [error] #PID&#x3C;0.285.0> running Bypass.Plug (connection #PID&#x3C;0.283.0>, stream id 2) terminated
Server: localhost:3000 (http)
Request: GET /coffee/hot
** (exit) an exception was raised:
    ** (RuntimeError) route error
        (bypass 2.1.0) lib/bypass/plug.ex:28: Bypass.Plug.call/2
        (plug_cowboy 2.7.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_handler.erl
:37: :cowboy_handler.execute/2
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:306: :cowboy_stream_h.execute/3
        (cowboy 2.12.0) /home/iago-effting/code/study/book/coffee_shop/deps/cowboy/src/cowboy_stream_h.er
l:295: :cowboy_stream_h.request_process/3
        (stdlib 5.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3


  1) test all_hot_coffees/0 recovery of too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/coffee_shop/integrations/coffee/client_test.exs:31
     match (=) failed
     code:  assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
     left:  {:ok, %CoffeeShop.Integrations.Coffee.Response{status: status}}
     right: {
              :error,
              %CoffeeShop.Integrations.Coffee.Response{
<strong>                status: 500,
</strong>                body: %{},
                error: "Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"
              }
            }
     stacktrace:
       test/coffee_shop/integrations/coffee/client_test.exs:40: (test)


Finished in 1.0 seconds (0.00s async, 1.0s sync)
3 tests, 1 failure, 2 excluded




</code></pre>

Recebemos um status 500. Isso foi uma falha em nosso serviço falso. O culpado é a função do `Bypass.expect_once/4` isso acontece por que o *Bypass* esperava apenas uma chamada dessa requisição (expect\_once), mas recebemos mais do que uma, comprovando que nosso *retry* funcionou.

*Bypass* possui a função `Bypass.expect/4` que funciona igual ao `expect_once/4`, mas sem o limitador de ser apenas uma vez. Vamos substituir ela por apenas `expect/4` e o resto continua igual

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    # ...
    
    test "too much requests", %{bypass: bypass} do
<strong>      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Se rodarmos o teste novamente, iremos perceber duas coisas.

1. O teste ainda falha (o que faz sentido, a resposta das duas requisições é o status 429
2. O teste ficou mais lento.

Vamos resolver a primeira parte. A resposta para as duas requisições em nosso serviço falso é está sendo o 429:&#x20;

```elixir
Plug.Conn.resp(conn, 429, "")
```

Isso quer dizer, nunca receberemos o 200 dessa forma. Precisamos criar um mecanismo que entenda a quantidade de requisições feitas nesse teste. Infelizmente nem o bypass nem o Tesla nos ajudam nessa hora. Não possui uma forma simples de saber, uma vez que o *conn* vindo do *bypass* não se comunica diretamente com a resposta do mesmo. Iremos precisar de algo que rode internamente no *bypass* e que possamos obter o resultado de contagem fora dele. Podemos usar um [Agent](https://hexdocs.pm/elixir/1.12.3/Agent.html).

Agents são uma abstração em torno de um GenServer. GenServer mantem estado e podemos resgatar pelo PID. Usando Agente, não precisamos do PID e sim, apenas da instância. Exatamente o que precisamos. Vamos usar o exemplo que tem na  documentação, é exatamente um contador =D

Criarei ele em `lib/integrations/couter.ex`

{% code title="lib/integrations/couter.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end
```

{% endcode %}

Podemos agora usar as funções

* `Counter.start_link/1` para iniciar nosso processo
* `Counter.increment/0` para adicionar 1 ao contador
* `Couter.value/0` para obter o valor total incrementado

Vamos adicionar esse ao nosso teste.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    test "too much requests", %{bypass: bypass} do
<strong>      Counter.start_link(0)
</strong>
      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        IO.inspect(Counter.value())
</strong>        
<strong>        Counter.increment()
</strong>        Plug.Conn.resp(conn, 429, "")
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

Os pontos importantes ali são.

* Iniciamos o *Counter* fora da chamada do *Bypass*, bem no inicio do teste.
* Incrementamos toda vez que o *Bypass* identificar uma nova requisição
* Adicionei também um `IO.inspect/1` para vermos o valor aparecer em nosso console.

Podemos rodar o teste agora e veremos o valor sendo incrementado

```
mix test
0
1
2
3


  1) test all_hot_coffees/0 too much requests (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:45
     Assertion with == failed
     code:  assert status == 200
     left:  429
     right: 200
     stacktrace:
       test/integrations/coffee/client_test.exs:60: (test)

.....
Finished in 4.7 seconds (0.00s async, 4.7s sync)
1 doctest, 5 tests, 1 failure
```

A contagem ficou de 0, 1, 2 e 3, Isso quer dizer, o 0 foi a primeira requisição e quando ele falhou começou a processar os *retry*, tendo 3 chances para se recuperar, ao chegar no limite máximo ele simplesmente ficou com a resposta da última tentativa. Percebe como nossos *retries* estão funcionando bem? Falta apenas uma logica para mockarmos o retorno de uma desses retries de forma a ser um sucesso.

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    #...
    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)
      
      response =
        File.read!("test/coffee_shop/integrations/coffee/fixtures/success.json")

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
<strong>        case Counter.value() do
</strong><strong>          0 = _first_call ->
</strong><strong>            Counter.increment()
</strong><strong>            Plug.Conn.resp(conn, 429, "Too many requests")
</strong><strong>
</strong><strong>          1 = _first_retry_call ->
</strong><strong>            Plug.Conn.resp(conn, 200, response)
</strong><strong>        end
</strong>      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)

      assert status == 200
    end
  end
end

</code></pre>

O case é utilziado para saber qual resposta devolvemos. Simulamos ali, uma resposta com status 429 e em seguida, com o retry, uma nova resposta com o 200. Isso garante que o retry está funcionando e que temos tratamento para 429.

```
mix test
```

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.8 seconds (0.00s async, 1.8s sync)
1 doctest, 5 tests, 0 failures
```

Lindo não?

## O problema do Delay

Temos um pequeno problema no nosso código, para ser especifico, um problema em rodar os testes. Façamos um experimento. Entre no seu cliente e altere o valor do delay e max\_delay para 10\_000:

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: 10_000,
</strong>       max_retries: 3,
<strong>       max_delay: 10_000,
</strong>       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Rode esse teste e veja que o teste irá passar.&#x20;

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 18.1 seconds (0.00s async, 18.1s sync)
1 doctest, 5 tests, 0 failures
```

Pareceu até ter travado certo? O teste demorou 18 segundos. Nosso *delay* esta funcionando corretamente, inclusive o teste passa como deveria. Mas isso causa um problema de fluxo de trabalho. Imagina termos mais 10 clientes que também precisamos fazer o teste de *retry*. Nossa suite de teste ficaria lenta.

Precisamos de uma forma de configurar o delay em nossos testes, para não comprometer nossa performance.

Um minuto de agradecimento a nós do passado, que criamos um mecanismo de configuração. Podemos então fazer o mesmo que fizemos com a configuração da URL base.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")
<strong>    delay_to_retry = Keyword.get(opts, :delay_to_retry, 1000)
</strong>
    middlewares = [
      {Tesla.Middleware.Retry,
<strong>       delay: delay_to_retry,
</strong>       max_retries: 3,
       max_delay: 3000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end
</code></pre>

Agora basta passar a configuração por parâmetro:

<pre class="language-elixir" data-title="test/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  describe "all_hot_coffees/0" do
    # ...

    test "too much requests", %{bypass: bypass} do
      Counter.start_link(0)

      Bypass.expect(bypass, "GET", "/coffee/hot", fn conn ->
        case Counter.value() do
          0 = _first_call ->
            Counter.increment()
            Plug.Conn.resp(conn, 429, "")

          1 = _first_retry_call ->
            Plug.Conn.resp(conn, 200, "")
        end
      end)

      opts = [
        base_url: "http://localhost:3000",
<strong>        delay_to_retry: 1
</strong>      ]

      assert {:ok, %Response{status: status}} = Client.all_hot_coffees(opts)
      assert status == 200
    end
  end
end
</code></pre>

```
> time mix test
Compiling 1 file (.ex)
......
Finished in 1.0 seconds (0.00s async, 1.0s sync)
1 doctest, 5 tests, 0 failures
```

Teste finalizado em 1 segundo. Muito melhor.

## Refactoring

Por ultimo, vamos dar uma mexida em nosso cliente e deixa-lo melhor. Nossa função de requisição ficou grande e mistura chamada com configuração. Caso eu queira criar uma nova função para requisitar outro *endpoint*, teremos que duplicar a configuração, isso não é bom.&#x20;

Vamos resolver isso extraindo para uma função de criação de client. Vou chama-lo de `new_client/1` . Ela vai ser privada para apenas utilizarmos dentro do modulo. Para configurar o cliente, basta passar as opções de configurações por parâmetro. Sua resposta é um  `Tesla.Client` configurado que poderá ser usado em qualquer requisição.

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

<strong>  defp new_client(opts \\ []) do
</strong><strong>    middlewares = [
</strong><strong>      {Tesla.Middleware.Retry,
</strong><strong>       delay: Keyword.get(opts, :delay_to_retry, 1000),
</strong><strong>       max_retries: 3,
</strong><strong>       max_delay: 20_000,
</strong><strong>       should_retry: fn
</strong><strong>         {:ok, %{status: status}} when status in [429] -> true
</strong><strong>         {:ok, _} -> false
</strong><strong>         {:error, _} -> true
</strong><strong>       end}
</strong><strong>    ]
</strong><strong>
</strong><strong>    Tesla.client(middlewares)
</strong><strong>  end
</strong>
  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, "https://api.sampleapis.com")

    opts
<strong>    |> new_client()
</strong>    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
end

</code></pre>

Muito melhor. Também gosto de separar a URL base para facilitar leitura.&#x20;

<pre class="language-elixir" data-title="lib/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  defp new_client(opts \\ []) do
    middlewares = [
      {Tesla.Middleware.Retry,
       delay: Keyword.get(opts, :delay_to_retry, 1000),
       max_retries: 3,
       max_delay: 20_000,
       should_retry: fn
         {:ok, %{status: status}} when status in [429] -> true
         {:ok, _} -> false
         {:error, _} -> true
       end}
    ]

    Tesla.client(middlewares)
  end

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end
  
<strong>  defp base_url(), do: "https://api.sampleapis.com"
</strong>end

</code></pre>

Com isso, temos um código legivel e prático.


# Rate Limit de longa duração


# Agendando uma nova tentativa de requisição

Algumas vezes não precisamos que a requisição seja feito na hora. Por exemplo, sincronizar a lista de cafés, para caso algo mudar ou ser adicionado não precisa ser feito na hora. Claro que se conseguirmos fazer diretamente, seria interessante. Mas, nem sempre é possível. E pior, podemos ter chego na quantidade máxima de requisições do dia. Antes, para solucionar o *Rate Limit* de requisições por segundo, apenas criamos um mecanismo de espera. Nesse caso não vai ser interessante. Não vamos querer fazer o usuário ficar 20h esperando a resposta, certo? O que podemos fazer é adicionar nossa requisição em uma fila, que aguarda por exemplo, um dia para ser executada e deixar isso de forma assíncrona. Ganhamos com:

1. Não deixamos o usuário preso na tela
2. Resolvemos o problema do *Rate Limit* com longa duração (como por exemplo, um dia)
3. Temos o dado atualizado após essa execução.

Precisamos de alguns requisitos para essa estratégia funcionar.

### Número 01

Para realizar a requisitações de forma assincrona, precisamos de toda os dados armazenados para executar quando a hora chegar. Caso você queira que o café com ID 1 seja atualizado, precisamos do ID na hora da sincronia. Para isso, precisaremos de uma ferramenta de armazenamento. Utilizaremos o banco de dados ***Postgres*** para manter padrão de mercado.&#x20;

Tendo escolhido o banco de dados, precisamos nos comunicar com ele. Seguindo também o padrão de mercado, utilizaremos o *Ecto*. Provavelmente você já usou ou já ouviu falar sobre ele.

### Número 02

Precisamos de uma ferramentas que cuide do agendamento e rode os *jobs* quando a hora chegar. Para fazer isso, iremos utilizar uma ferramenta chamada [Oban](https://hexdocs.pm/oban/installation.html) e agendar a sincronia.

Nossa lista ficou:

* [Configurar *Ecto* para se comunicar com *Postgres*](/problemas-de-api-externa/rate-limit-de-longa-duracao/adicionando-ecto-ao-projeto)
* [Configurar *Oban*](/problemas-de-api-externa/rate-limit-de-longa-duracao/instalando-oban)


# Configurações necessárias

Você precisará instalar o banco de dados *Postgress*. Pode ser via terminal ou via *docker*. O importante é termos ele. Existem diversos tutoriais na internet sobre, então não vou repetir aqui.&#x20;

Depois disso, precisamos configurar ele em nossa aplicação, e para isso utilizarei um wrapper de banco de dados. O famoso [Ecto](https://hexdocs.pm/ecto/getting-started.html).

Mas antes disso, precisamos atualizar nosso app para ter suporte a árvore de supervisão.

## Suporte árvore de supervisão

*Ecto* trabalha como um *wrapper* de banco de dados. Isso quer dizer que podemos fazer queries com ele utilizando uma linguagem mais clara. Um detalhes importante, o *Ecto* deve ser rodado como um supervisor. Isso quer dizer, nossa aplicação deve ter suporte a uma [árvore de supervisão](https://hexdocs.pm/elixir/supervisor-and-application.html). Talvez você esteja acostumado com essa nomenclatura, infelizmente não tenho espaço no livro para falar sobre isso aqui. Deixei um link para você se aprofundar no estudo. Nós aqui, seguiremos no projeto.

Nossa aplicação não possui uma árvore de supervisão. O primeiro passo de configuração é transformar ela em uma. Caso queira pular essa etapa, você pode criar um novo projeto ja adicionando a flag para ser uma aplicação com a árvore.

```
mix new [nome-do-projeto] --sup
```

Utilizando a flag `--sup` o novo projeto automaticamente terá suporte. Mas eu farei a mudança na mão. Caso tenha usado a flag, você pode pular para a seção [Adicionando Ecto ao projeto](/problemas-de-api-externa/rate-limit-de-longa-duracao/adicionando-ecto-ao-projeto).

## Adicionando árvore de supervisão

Vamos começar criando um módulo novo. Por convenção chamarei de *Application:*

<pre class="language-elixir" data-title="lib/coffee_shop/application.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Application do
  use Application

  def start(_type, _args) do
    children = []
    opts = [strategy: :one_for_one, name: CoffeeShop.Supervisor]

<strong>    Supervisor.start_link(children, opts)
</strong>  end
end
</code></pre>

Na linha 8 iniciamos o supervisor de nossa aplicação e passamos outros supervisores filhos. Adicionaremos depois o *Ecto* nesse pedaço.

Agora precisamos adicionar o supevisor de nossa aplicação ao nosso projeto. Vamos até o mix.exs e adicionar uma nova opção `mod` na função `application/0` passando o módulo Application que criamos.

<pre class="language-elixir" data-title="mix.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.MixProject do
  use Mix.Project

  # ...
  
  def application do
    [
      extra_applications: [:logger],
<strong>      mod: {CoffeeShop.Application, []}
</strong>    ]
  end

  # ...
end
</code></pre>

Como segundo item da tupla adicionaremos uma lista vazia. Esse valor sçao argumentos que podemos passar para dentro do processo. Não temos necessidade de fazer isso, então, continuará vazio.

Pronto, nosso projeto agora tem suporte a árvore de supervisão. Podemos seguir.


# Adicionando Ecto ao projeto

## Adicionando Ecto ao projeto

Temos a página de configuração [nesse link](https://hexdocs.pm/ecto/getting-started.html). Mas farei a configuração por aqui também. Primeiro precisamos instalar as dependências

* ecto\_sql -> Wrapper do banco de dados
* postgrex -> Adaptador do Ecto para Postgress

Adicione a suas dependencias os dois.

<pre class="language-elixir" data-title="mix.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.MixProject do
  use Mix.Project

  def project do
    [
      app: :coffee_shop,
      version: "0.1.0",
      elixir: "~> 1.15",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {CoffeeShop.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:tesla, "~> 1.4"},
      {:bypass, "~> 2.1"},
<strong>      {:ecto_sql, "~> 3.0"},
</strong><strong>      {:postgrex, ">= 0.0.0"}
</strong>    ]
  end
end
</code></pre>

Depois basta gerir as dependências

```
mix deps.get
```

```
> mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.125s
New:
  db_connection 2.6.0
  decimal 2.1.1
  ecto 3.11.2
  ecto_sql 3.11.1
  postgrex 0.17.5
Unchanged:
  bypass 2.1.0
  cowboy 2.12.0
  cowboy_telemetry 0.4.0
  cowlib 2.13.0
  mime 2.0.5
  plug 1.15.3
  plug_cowboy 2.7.1
  plug_crypto 2.0.0
  ranch 1.8.0
  telemetry 1.2.1
  tesla 1.8.0
* Getting ecto_sql (Hex package)
* Getting postgrex (Hex package)
* Getting db_connection (Hex package)
* Getting decimal (Hex package)
* Getting ecto (Hex package)
```

Feito isso, vamos a configuração

Quando instalamos *Ecto*, ganhamos também alguns geradores que vão nos auxiliar.  O comando irá gerar a configuração necessária para nos conectar ao banco de dados.

```
mix ecto.gen.repo -r CoffeeShop.Repo
```

```elixir
> mix ecto.gen.repo -r CoffeeShop.Repo
* creating lib/coffee_shop
* creating lib/coffee_shop/repo.ex
* creating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/coffee_shop/application.ex):

    def start(_type, _args) do
      children = [
        CoffeeShop.Repo,
      ]

And to add it to the list of Ecto repositories in your
configuration files (so Ecto tasks work as expected):

    config :coffee_shop,
      ecto_repos: [CoffeeShop.Repo]
```

Ele gerou o arquivo `lib/coffee_shop/repo.ex` com a configuração para o banco de dados. Também foi adicionadm em `config/config.exs` a configuração de acesso ao banco. E por fim, nos avisou que  precisamos fazer duas coisas:

1. Adicionar ao supervisor de nossa aplicação o módulo criado `CoffeeShop.Repo`
2. Adicionar a configuraçãao em`config/config.exs`

Vamos adicionar o supevisor do Ecto ao supervisor de nossa aplicação. Abra `lib/coffee_shop/application.ex` que criamos para dar suporte a árvore de supervisor e adicione o módulo `Repo` a lista `children`.

<pre class="language-elixir" data-title="lib/coffee_shop/application.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Application do
  use Application

  def start(_type, _args) do
    children = [
<strong>      CoffeeShop.Repo
</strong>    ]

    opts = [strategy: :one_for_one, name: CoffeeShop.Supervisor]

    Supervisor.start_link(children, opts)
  end
end

</code></pre>

Com isso temos o `Repo` como filho de nosso supervisor.&#x20;

Agora vamos finalizar a configuração, adicionando a configuração de nosso repositório ecto em `config/confg.exs`

<pre class="language-elixir" data-title="config/config.exs" data-line-numbers><code class="lang-elixir">import Config

<strong>config :coffee_shop,
</strong><strong>  ecto_repos: [CoffeeShop.Repo]
</strong>
config :coffee_shop, CoffeeShop.Repo,
  database: "coffee_shop",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"
</code></pre>

Adicionado a linha 3, temos tudo configurado.

{% hint style="info" %}
A configuração da linha 6 vai depender de como você configurou seu banco de dados.
{% endhint %}

Feito isso, temos o *Ecto* configurado para nosso banco de dados. Agora vamos criar nosso banco. Tendo a adição do *Ecto* ao projeto, podemos simplesmente criar nosso banco usando o comando&#x20;

```
mix ecto.create
```

```
> mix ecto.create                     
Compiling 6 files (.ex)
Generated coffee_shop app
The database for CoffeeShop.Repo has been created
```

Nossa aplicação tem suporte a banco de dados *postgres*. Matamos então mais um passo

* [~~Configurar *Ecto* para se comunicar com *Postgres*~~](/problemas-de-api-externa/rate-limit-de-longa-duracao/adicionando-ecto-ao-projeto)
* [Configurar *Oban*](/problemas-de-api-externa/rate-limit-de-longa-duracao/instalando-oban)

Vamos lá.


# O que é o Oban

Oban é uma biblioteca de processamento de Jobs que usa PostgreSQL ou SQLite3 para armazenamento.


# Instalando Oban

A instação do Oban é bem simples, basta seguir o guia. Primeiro, vamos adicionar a dependência.

<pre class="language-elixir" data-title="mix.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.MixProject do
  use Mix.Project

  # ...

  defp deps do
    [
      {:tesla, "~> 1.4"},
      {:bypass, "~> 2.1"},
      {:ecto_sql, "~> 3.0"},
      {:postgrex, ">= 0.0.0"},
<strong>      {:oban, "~> 2.17"}
</strong>    ]
  end
end
</code></pre>

```
mix deps.get
```

```
> mix deps.get                                                                          
Resolving Hex dependencies...
Resolution completed in 0.06s
New:
  jason 1.4.1
  oban 2.17.8
Unchanged:
  bypass 2.1.0
  cowboy 2.12.0
  cowboy_telemetry 0.4.0
  cowlib 2.13.0
  db_connection 2.6.0
  decimal 2.1.1
  ecto 3.11.2
  ecto_sql 3.11.1
  mime 2.0.5
  plug 1.15.3
  plug_cowboy 2.7.1
  plug_crypto 2.0.0
  postgrex 0.17.5
  ranch 1.8.0
  telemetry 1.2.1
  tesla 1.8.0
* Getting oban (Hex package)
* Getting jason (Hex package)
You have added/upgraded packages you could sponsor, run `mix hex.sponsor` to learn more
```

Oban utiliza a estratégia de migrações utilizando *Ecto*. Para isso, precisamos criar um nova migração utilizando [Ecto](https://hexdocs.pm/ecto_sql/Ecto.Migration.html).

```
mix ecto.gen.migration add_oban_jobs_table
```

```
* creating priv/repo/migrations
* creating priv/repo/migrations/20240415200026_add_oban_jobs_table.exs
```

Ele criará uma nova pasta chamada `priv/repo/migrations` que será onde nossas migrações irão viver. Também criará o arquivo de nosso migração. Vamos abrir ela. No meu caso é o arquivo `priv/repo/migrations/20240415200026_add_oban_jobs_table.exs`, mas para você terá um prefixo diferente, dependendo da data de criação.&#x20;

{% code title="priv/repo/migrations/20240415200026\_add\_oban\_jobs\_table.exs" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def change do

  end
end
```

{% endcode %}

Primeiro vamos mudar de `change/0` para `up/0`, fazemos isso porque queremos que tenha o `up/0` e o `down/0`. `up/0` para rodar a migração e `down/0` para fazer o *rollback* caso necessário. Na função `up/0` iremos adicionar a configuração por meio de uma função e no `down/0`, fazermos o *downgrade* para a versão 1 zerando as dependência.&#x20;

<pre class="language-elixir" data-title="priv/repo/migrations/20240415200026_add_oban_jobs_table.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Repo.Migrations.AddObanJobsTable do
  use Ecto.Migration

  def up do
<strong>    Oban.Migration.up(version: 12)
</strong>  end

  def down do
<strong>    Oban.Migration.down(version: 1)
</strong>  end
end

</code></pre>

Precisamos agora rodar as migrações pendentes usando o comando do Ecto

```elixir
mix ecto.migrate
```

```
> mix ecto.migrate                          

17:07:47.213 [info] == Running 20240415200026 CoffeeShop.Repo.Migrations.AddObanJobsTable.up/0 forward

17:07:47.243 [info] execute "DO $$\nBEGIN\nIF NOT EXISTS (SELECT 1 FROM pg_type\n               WHERE typname = 'oban_job_state'\n                 AND typnamespace = 'public'::regnamespace::oid) THEN\n    CREATE TYPE \"public\".oban_job_state AS ENUM (\n      'available',\n      'scheduled',\n      'executing',\n      'retryable',\n      'completed',\n      'discarded'\n    );\n  END IF;\nEND$$;\n"

17:07:47.249 [info] create table if not exists public.oban_jobs

17:07:47.259 [info] create index if not exists public.oban_jobs_queue_index

17:07:47.263 [info] create index if not exists public.oban_jobs_state_index

17:07:47.267 [info] create index if not exists public.oban_jobs_scheduled_at_index

17:07:47.272 [info] execute "CREATE OR REPLACE FUNCTION \"public\".oban_jobs_notify() RETURNS trigger AS $$\nDECLARE\n  channel text;\n  notice json;\nBEGIN\n  IF (TG_OP = 'INSERT') THEN\n    channel = 'public.oban_insert';\n    notice = json_build_object('queue', NEW.queue, 'state', NEW.state);\n\n    -- No point triggering for a job that isn't scheduled to run now\n    IF NEW.scheduled_at IS NOT NULL AND NEW.scheduled_at > now() AT TIME ZONE 'utc' THEN\n      RETURN null;\n    END IF;\n  ELSE\n    channel = 'public.oban_update';\n    notice = json_build_object('queue', NEW.queue, 'new_state', NEW.state, 'old_state', OLD.state);\n  END IF;\n\n  PERFORM pg_notify(channel, notice::text);\n\n  RETURN NULL;\nEND;\n$$ LANGUAGE plpgsql;\n"

17:07:47.274 [info] execute "DROP TRIGGER IF EXISTS oban_notify ON \"public\".oban_jobs"

17:07:47.276 [info] trigger "oban_notify" for relation "public.oban_jobs" does not exist, skipping

17:07:47.276 [info] execute "CREATE TRIGGER oban_notify\nAFTER INSERT OR UPDATE OF state ON \"public\".oban_jobs\nFOR EACH ROW EXECUTE PROCEDURE \"public\".oban_jobs_notify();\n"

17:07:47.279 [info] drop index if exists public.oban_jobs_scheduled_at_index

17:07:47.281 [info] create index public.oban_jobs_scheduled_at_index

17:07:47.285 [info] create check constraint worker_length on table public.oban_jobs

17:07:47.287 [info] create check constraint queue_length on table public.oban_jobs

17:07:47.289 [info] execute "CREATE OR REPLACE FUNCTION \"public\".oban_wrap_id(value bigint) RETURNS int AS $$\nBEGIN\n  RETURN (CASE WHEN value > 2147483647 THEN mod(value, 2147483647) ELSE value END)::int;\nEND;\n$$ LANGUAGE plpgsql IMMUTABLE;\n"

17:07:47.291 [info] alter table public.oban_jobs

17:07:47.293 [info] execute "DROP FUNCTION IF EXISTS \"public\".oban_wrap_id(value bigint)"

17:07:47.294 [info] drop index if exists public.oban_jobs_scheduled_at_index

17:07:47.296 [info] drop index if exists public.oban_jobs_queue_index

17:07:47.297 [info] drop index if exists public.oban_jobs_state_index

17:07:47.299 [info] create index if not exists public.oban_jobs_queue_state_scheduled_at_id_index

17:07:47.303 [info] create index if not exists public.oban_jobs_attempted_at_id_index

17:07:47.307 [info] alter table public.oban_jobs

17:07:47.309 [info] alter table public.oban_jobs

17:07:47.312 [info] drop index if exists public.oban_jobs_queue_state_scheduled_at_id_index

17:07:47.314 [info] create index if not exists public.oban_jobs_state_queue_priority_scheduled_at_id_index

17:07:47.318 [info] execute "CREATE OR REPLACE FUNCTION \"public\".oban_jobs_notify() RETURNS trigger AS $$\nDECLARE\n  channel text;\n  notice json;\nBEGIN\n  IF NEW.state = 'available' THEN\n    channel = 'public.oban_insert';\n    notice = json_build_object('queue', NEW.queue);\n\n    PERFORM pg_notify(channel, notice::text);\n  END IF;\n\n  RETURN NULL;\nEND;\n$$ LANGUAGE plpgsql;\n"

17:07:47.320 [info] execute "DROP TRIGGER IF EXISTS oban_notify ON \"public\".oban_jobs"

17:07:47.322 [info] execute "CREATE TRIGGER oban_notify\nAFTER INSERT ON \"public\".oban_jobs\nFOR EACH ROW EXECUTE PROCEDURE \"public\".oban_jobs_notify();\n"

17:07:47.323 [info] alter table public.oban_jobs

17:07:47.331 [info] execute "DO $$\nDECLARE\n  version int;\n  already bool;\nBEGIN\n  SELECT current_setting('server_version_num')::int INTO version;\n  SELECT '{cancelled}' <@ enum_range(NULL::\"public\".oban_job_state)::text[] INTO already;\n\n  IF already THEN\n    RETURN;\n  ELSIF version >= 120000 THEN\n    ALTER TYPE \"public\".oban_job_state ADD VALUE IF NOT EXISTS 'cancelled';\n  ELSE\n    ALTER TYPE \"public\".oban_job_state RENAME TO old_oban_job_state;\n\n    CREATE TYPE \"public\".oban_job_state AS ENUM (\n      'available',\n      'scheduled',\n      'executing',\n      'retryable',\n      'completed',\n      'discarded',\n      'cancelled'\n    );\n\n    ALTER TABLE \"public\".oban_jobs RENAME column state TO _state;\n    ALTER TABLE \"public\".oban_jobs ADD state \"public\".oban_job_state NOT NULL default 'available';\n\n    UPDATE \"public\".oban_jobs SET state = _state::text::\"public\".oban_job_state;\n\n    ALTER TABLE \"public\".oban_jobs DROP column _state;\n    DROP TYPE \"public\".old_oban_job_state;\n  END IF;\nEND$$;\n"

17:07:47.333 [info] create index if not exists public.oban_jobs_state_queue_priority_scheduled_at_id_index

17:07:47.335 [info] relation "oban_jobs_state_queue_priority_scheduled_at_id_index" already exists, skipping

17:07:47.335 [info] alter table public.oban_jobs

17:07:47.340 [info] create check constraint priority_range on table public.oban_jobs

17:07:47.341 [info] create check constraint positive_max_attempts on table public.oban_jobs

17:07:47.343 [info] create check constraint attempt_range on table public.oban_jobs

17:07:47.345 [info] drop index if exists public.oban_jobs_args_vector

17:07:47.346 [info] index "oban_jobs_args_vector" does not exist, skipping

17:07:47.346 [info] drop index if exists public.oban_jobs_worker_gist

17:07:47.348 [info] index "oban_jobs_worker_gist" does not exist, skipping

17:07:47.348 [info] drop index if exists public.oban_jobs_attempted_at_id_index

17:07:47.349 [info] create index if not exists public.oban_jobs_args_index

17:07:47.351 [info] create index if not exists public.oban_jobs_meta_index

17:07:47.353 [info] create table if not exists public.oban_peers

17:07:47.360 [info] execute "ALTER TABLE \"public\".oban_peers SET UNLOGGED"

17:07:47.371 [info] drop constraint priority_range from table public.oban_jobs

17:07:47.373 [info] create check constraint non_negative_priority on table public.oban_jobs

17:07:47.374 [info] execute "DROP TRIGGER IF EXISTS oban_notify ON \"public\".oban_jobs"

17:07:47.376 [info] execute "DROP FUNCTION IF EXISTS \"public\".oban_jobs_notify()"

17:07:47.377 [info] execute "COMMENT ON TABLE \"public\".oban_jobs IS '12'"

17:07:47.381 [info] == Migrated 20240415200026 in 0.1s
```

Com isso criamos todas as tabelas necessárias para rodar o Oban. Agora iremos configurar sua utilização no projeto. Abra o arquivo `config/config.exs`

<pre class="language-elixir" data-title="config/config.exs" data-line-numbers><code class="lang-elixir">import Config

config :coffee_shop,
  ecto_repos: [CoffeeShop.Repo]

config :coffee_shop, CoffeeShop.Repo,
  database: "coffee_shop_repo",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"
  
<strong>config :coffee_shop, Oban,
</strong><strong>  engine: Oban.Engines.Basic,
</strong><strong>  queues: [default: 10],
</strong><strong>  repo: CoffeeShop.Repo
</strong></code></pre>

Temos algumas configurações básicas da engine. Nela temos a quantidade por fila e o repositório que armazenaremos nossos *jobs*. Feito isso, pronto para produção, mas não para testes.

Imagine rodar o Oban normalmente e o test ficar preso por horas por causa de um job que tem duração longa. Precisamos criar um tipo de *mock* para isso. O próprio Oban nos da esse super poder, precisando apenas so configurar ele no ambiente de teste. Para isso, vamos criar um arquivo em `config/test.exs`

{% code title="config/test.exs" lineNumbers="true" %}

```elixir
config :coffee_shop, Oban, testing: :inline
```

{% endcode %}

A hierarquia de configurações ficará

1. Obtem as configuraçĩoes de config/config.ex
2. Estamos em ambietne de test, pegue as configurações de test e o que tiver de igual o de test mantem, sobrepondo o de config/config.exs.

Finalmente precisamos adicionar o *Oban* a nossa arvore de supervisão, da mesma forma que  adicionamos nosso `CoffeeShope.Repo`. Vamos em `lib/coffee_shop/application.ex` e colocar um novo filho a supervisão.

<pre class="language-elixir" data-title="lib/coffee_shop/application.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Application do
  use Application

  def start(_type, _args) do
    children = [
      CoffeeShop.Repo,
<strong>      {Oban, Application.fetch_env!(:coffee_shop, Oban)}
</strong>    ]

    opts = [strategy: :one_for_one, name: CoffeeShop.Supervisor]

    Supervisor.start_link(children, opts)
  end
end
</code></pre>

Utilizamos `Application.fetch_env!/2` para obter a configuração dos arquivos de configuração.

Pronto. Para conseguir ver se tudo deu certo basta acessar o terminal iterativo e rodar `Oban.config:`

```
iex -S mix
```

```elixir
Oban.config()
```

```elixir
iex(1)> Oban.config()
%Oban.Config{
  dispatch_cooldown: 5,
  engine: Oban.Engines.Basic,
  get_dynamic_repo: nil,
  insert_trigger: true,
  log: false,
  name: Oban,
  node: "iago-effting",
  notifier: {Oban.Notifiers.Postgres, []},
  peer: {Oban.Peers.Postgres, []},
  plugins: [],
  prefix: "public",
  queues: [default: [limit: 10]],
  repo: CoffeeShop.Repo,
  shutdown_grace_period: 15000,
  stage_interval: 1000,
  testing: :disabled
}
```

* [~~Configurar *Ecto* para se comunicar com *Postgres*~~](/problemas-de-api-externa/rate-limit-de-longa-duracao/adicionando-ecto-ao-projeto)
* [~~Configurar *Oban*~~](/problemas-de-api-externa/rate-limit-de-longa-duracao/instalando-oban)

Ótimo, Oban a postos.


# Criando uma requisição assíncrona

Primeiro iremos criar uma nova requisição. Utilizaremos o mesmo recurso para fins de estudo, mas a estratégia de execução será diferente.

* Precisamos da atualização dos cafés quentes;
* Temos a limitação de 50 requisições por dia no *Rate Limit*;
* Não precisamos ter uma resposta imediata, isso pode ser atualizado quando der.

Tendo isso em mente, criaremos uma requisição que bate no mesmo endpoint dos cafés quentes, porém, não iremos esperar uma resposta imediata e sim, um :ok, nos avisando que o agendamento esta pronto.&#x20;

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response

  # ...

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end

<strong>  def all_hot_coffees_async(opts \\ []) do
</strong><strong>    
</strong><strong>  end
</strong>
  defp base_url(), do: "https://api.sampleapis.com"
end

</code></pre>

usei o mesmo nome da função, com adição do sufixo `_assync`, indicando assincronicidade.  Isso quer dizer, não esperamos que isso seja feito agora e sim, de forma assincrona.

Diferente da primeira função, na segunda precisamos agendar um [Job ](https://hexdocs.pm/oban/Oban.Job.html)para ser executado. Para isso, precisamos primeiro do [Worker](https://hexdocs.pm/oban/Oban.Worker.html) que irá processar nosso [Job](https://hexdocs.pm/oban/Oban.Job.html). Vamos criar o *worker* com o nome `HotCoffeesWorker` que fara a sincronia de forma assíncrona para nos.

A estrutura do nosso modulo é bem simples, utilizaremos a macro `use Oban.Worke`r e precisaremos implementar a função `perform/1` .

{% code title="lib/coffee\_shop/integrations/coffee/hot\_coffees\_worker.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.Coffee.HotCoffeesWorker do
  use Oban.Worker

  @impl Oban.Worker
  def perform(%Oban.Job{args: _args}) do
    :ok
  end
end

```

{% endcode %}

A função `perform/1` será executada quando um job rodar. Precisamos que ela realize a chamada para o serviço externo.  Para isso, vamos reaproveitar a função sincrona criada no cliente, `Client.all_hot_coffees/1.`

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/hot_coffees_worker.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.HotCoffeesWorker do
  use Oban.Worker

  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Coffee.Client

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"opts" => opts}}) do
<strong>    opts = to_list(opts)
</strong>    
<strong>    case Client.all_hot_coffees(opts) do
</strong><strong>      {:ok, %Response{status: 200, body: _body}} -> :ok
</strong><strong>      {:ok, %Response{status: status, body: _body}} -> {:error, "status: #{status}"}
</strong><strong>      error -> error
</strong>    end
  end
  
<strong>  defp to_list(map) do
</strong><strong>    Enum.map(map, fn {key, value} -> {String.to_existing_atom(key), value} end)
</strong><strong>  end
</strong>end
</code></pre>

O Oban salva os parâmetros como map, devido a não suportar uma lista. Por isso que ao receber o opts convertemos ele novamente para lista e o processamento continua igual o original utilizanod o *Keyword*.

O `case/1`  controla o que esperamos. Nesse caso, sera apenas um sucesso quando recebermos um **status 200**. Fora isso, queremos que seja um erro.

Nosso *worker* está pronto, agora precisamos agendar a execução. Para isso utilizaremos a função `Oban.insert/1` que espera um *Job*. Para conseguirmos o *Job* de forma fácil, utilizaremos a macro em nosso modulo *worker* e executaremos a função `HotCoffeesWorker.new/1` que retorna um *Job* já configurado para nosso *Worker*. Vamos replicar isso em nosso cliente.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response
<strong>  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
</strong>  
  # ...

  def all_hot_coffees(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/coffee/hot")
    |> Response.build()
  end

<strong>  def all_hot_coffees_async(opts \\ []) do
</strong><strong>    opts
</strong><strong>    |> Map.new(fn option -> option end)
</strong><strong>    |> HotCoffeesWorker.new()
</strong><strong>    |> Oban.insert()
</strong><strong>  end
</strong>
  defp base_url(), do: "https://api.sampleapis.com"
end
</code></pre>

{% hint style="info" %}
Na linha 18, convertemos a lista em Map. Como informamos que aconteceria na criação do worker.
{% endhint %}

Ao rodar a função `all_hot_coffees_async/1` receberemos um `{:ok, _job}` de resposta e não mais a estrutura `%Response{}`, isso acontece porque estamos criando um novo *Job* e não mais realizando uma requisição para o serviço. Vamos criar um teste comprovando nossa ideia.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
<strong>  use Oban.Testing, repo: CoffeeShop.Repo
</strong>
  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  # ...
  
<strong>  describe "all_hot_coffees_async/0" do
</strong><strong>    test "schedule a job" do
</strong><strong>      opts = [
</strong><strong>        base_url: "http://localhost:3000",
</strong><strong>        retry_delay: 1
</strong><strong>      ]
</strong><strong>
</strong><strong>      assert {:ok, _job} = Client.all_hot_coffees_async(opts)
</strong><strong>
</strong><strong>      # Verificando se foi agendado com sucesso
</strong><strong>      assert_enqueued(
</strong><strong>        worker: HotCoffeesWorker,
</strong><strong>        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1}
</strong><strong>      )
</strong><strong>    end
</strong>  end
end
</code></pre>

Você já pode rodar os testes do nosso cliente

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

```
> mix test test/coffee_shop/integrations/coffee/client_test.exs
....
Finished in 1.0 seconds (0.00s async, 1.0s sync)
4 tests, 0 failures
```

Agendamento realizado com sucesso. Mas precisamos de mais garantias. Nosso Rate Limit é de 24h. Isso quer dizer que precisamos garantir que essa execução só irá ser executada em 24h certo? No teste, nao temos essa garantia ainda, temos que adiciona-la.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  use Oban.Testing, repo: CoffeeShop.Repo

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

  # ...

  describe "all_hot_coffees_async/0" do
    test "schedule a job" do
      opts = [
        base_url: "http://localhost:3000",
        retry_delay: 1
      ]

      assert {:ok, _job} = Client.all_hot_coffees_async(opts)

<strong>      in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)
</strong>
      assert_enqueued(
        worker: HotCoffeesWorker,
        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
<strong>        scheduled_at: in_a_day
</strong>      )
    end
  end
end

</code></pre>

O próprio Oban nos disponibiliza na função `assert_enqueued` o `scheduled_at`, para confirmarmos para quando foi agendado. Com isso, criamos a variavel in\_a\_day  com a utilização do `DateTime` para adicionarmos o tempo a partir de agora + 24h e adicionamos na `assertion`. Vamos rodar e ver o que acontece.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
.

  1) test all_hot_coffees_async/0 schedule a job (CoffeeShop.Integrations.Coffee.ClientTest)
     test/coffee_shop/integrations/coffee/client_test.exs:73
     Expected a job matching:
     
     %{
       args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
       worker: CoffeeShop.Integrations.Coffee.HotCoffeesWorker,
       scheduled_at: ~U[2024-04-17 13:44:40.309948Z]
     }
     
     to be enqueued. Instead found:
     
     [
       %{
         args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
         worker: "CoffeeShop.Integrations.Coffee.HotCoffeesWorker",
         scheduled_at: ~U[2024-04-16 13:44:40.294598Z]
       }
     ]
     
     code: assert_enqueued(
     stacktrace:
       test/coffee_shop/integrations/coffee/client_test.exs:83: (test)

..
Finished in 1.2 seconds (0.00s async, 1.2s sync)
4 tests, 1 failure
```

A há, é um ótimo teste para se ter, não é? Esperamos é rodar o *job* em **2024-04-17,** mas está sendo executado um dia antes **2024-04-16**. Isso quer dizer, esta rodando logo quando é agendado.&#x20;

Não é o comportamento que esperamos. Isso está acontecendo porque realmente não configuramos essa etapa. Devemos configurar isso quando o *job*. Em nosso cliente é inserido o *job* e lá vamos adicionar essa opção:

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/hot_coffees_worker.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.Client do
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker

  # ...
  
  def all_hot_coffees_async(opts \\ []) do
<strong>   in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)
</strong>
    opts
    |> Map.new(fn option -> option end)
<strong>    |> HotCoffeesWorker.new(scheduled_at: in_a_day)
</strong>    |> Oban.insert()
  end

  # ...
end
</code></pre>

Criado a regra na linha 9 e adicionado a opção na linha 12. Agora rodaremos o teste novamente.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
Compiling 1 file (.ex)
....
Finished in 1.1 seconds (0.00s async, 1.1s sync)
4 tests, 0 failures
```

Estamos atendendo a regra que precisamos seguir com o *rate limit* de longa duração.

Em nosso teste, não temos garantia de que a execução do perform funciona, apenas garantimos o agendamento. Precisamos saber se conseguimos rodar o que foi agendado. Para isso usaremos a função `perform_job/2` que executará o job agendado.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  use Oban.Testing, repo: CoffeeShop.Repo

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.HotCoffeesWorker
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Counter

<strong>  setup do
</strong><strong>    bypass = Bypass.open(port: 3000)
</strong><strong>    {:ok, bypass: bypass}
</strong><strong>  end
</strong>
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees", %{bypass: bypass} do
      # ...
    end

    test "service is crashed", %{bypass: bypass} do
      # ...
    end

    test "too much requests", %{bypass: bypass} do
      # ...
    end
  end

  describe "all_hot_coffees_async/0" do
    test "schedule a job" do
<strong>      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
</strong><strong>        Plug.Conn.resp(conn, 200, "")
</strong><strong>      end)
</strong>      
      opts = [
        base_url: "http://localhost:3000",
        retry_delay: 1
      ]

      assert {:ok, _job} = Client.all_hot_coffees_async(opts)

      in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)

      assert_enqueued(
        worker: HotCoffeesWorker,
        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
        scheduled_at: in_a_day
      )
      
<strong>      assert %{success: 1} =
</strong><strong>               Oban.drain_queue(queue: :default, with_scheduled: true, with_safety: false)
</strong>    end
  end
end

</code></pre>

Adicionamos uma nova etapa na linha 50 que executa o job agendado. Sua resposta vem com a estrutura de map dos seguintes dados:

```elixir
%{failure: _, snoozed: _, success: _}
```

Para nosso teste passar, a execução deve ser um sucesso. por isso nossa resposta está com o pattern matching `%{success: 1}`.&#x20;

Rode seu teste e veja o resultado.

```
mix test test/coffee_shop/integrations/coffee/client_test.exs
```

```
> mix test test/coffee_shop/integrations/coffee/client_test.exs   
....
Finished in 1.1 seconds (0.00s async, 1.1s sync)
4 tests, 0 failures
```

Mais bonito que uma geladeira inox side-by-side com dispenser de gelo.


# Configurando quantidade de tentativas no Oban

Mesmo com o mecanismo de *retry* do *Tesla,* nossa requisição pode falhar e o Tesla não conseguir se recuperar. Para termos mais chance de sucesso, podemos configurar a quantidade de tentativas que o Oban poderá fazer antes de desistir. Por padrão ele tenta uma vez, mas podemos aumentar a quantidade. Nesse exemplo colocarei duas tentativas.&#x20;

Para configurar, vamos adicionar opções a nossa `Oban.Worker`:

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/coffee/hot_coffees_worker.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.HotCoffeesWorker do
<strong>  use Oban.Worker, max_attempts: 2
</strong>
  alias CoffeeShop.Integrations.Coffee.Response
  alias CoffeeShop.Integrations.Coffee.Client

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"opts" => opts}}) do
    opts = to_list(opts)
    
    case Client.all_hot_coffees(opts) do
      {:ok, %Response{status: 200, body: _body}} -> :ok
      {:ok, %Response{status: status, body: _body}} -> {:error, "status: #{status}"}
      error -> error
    end
  end

  defp to_list(map) do
    Enum.map(map, fn {key, value} -> {String.to_existing_atom(key), value} end)
  end
end
</code></pre>

Adicionamos a opção `max_attempts` para `2` . Agora precisamos testar se a configuração está correta.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/coffee/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.Coffee.ClientTest do

  # ...
  
  describe "all_hot_coffees_async/0" do
    test "schedule and execute a job", %{bypass: bypass} do
      Bypass.expect_once(bypass, "GET", "/coffee/hot", fn conn ->
        Plug.Conn.resp(conn, 200, "")
      end)

      opts = [
        base_url: "http://localhost:3000",
        retry_delay: 1
      ]

      in_a_day = DateTime.add(DateTime.utc_now(), 3600 * 24, :second)

<strong>      assert {:ok, job} = Client.all_hot_coffees_async(opts)
</strong><strong>      assert job.max_attempts == 2
</strong>
      assert_enqueued(
        worker: HotCoffeesWorker,
        args: %{"base_url" => "http://localhost:3000", "retry_delay" => 1},
        scheduled_at: in_a_day
      )

      args = %{opts: Map.new(opts, fn option -> option end)}

      assert :ok = perform_job(HotCoffeesWorker, args)
    end
  end
end

</code></pre>

Na linha 19 fazemos um *check* se o máximo de tentativas reflete o configurado. Deixamos o resto com o Oban.


# Level up

Uma das coisas que falei foi sobre a utilização de vários serviços para criar um novo. Por hora nos conectamos a apenas um terceiro que trás os cafezinhos.  Vamos ir para o próximo nível. Que acha de criar um produto que sugestione um cafezinho para tomar enquanto le um história em quadrinho da [Marvel](https://developer.marvel.com/docs).

Algo como:

```json
{
  "coffee": {
    "title": "Black Coffee",
    "image": "http://"
  },
  "comic": {
    "title": "Spider-man",
    "issue": 7
    "readingTime": "30 minutes",
    "link": "http://"
  }
}
```

Claro, seria legal termos um tempo de preparo no café ou talvez o tempo médio que leva para tomar. Mas podemos começar com o simples.

Temos alguns objetivos com isso

* Integrar em um novo serviço
* Unir os dois serviços.

Essa parte é o que importa. Integrar vários serviços e criar um novo com base nisso. É assim que a Web funcina.

Vamos nessa.


# Marvel API

Uma das coisas legais disso é que podemos brincar com várias APIs ao redor do mundo e encontrar a que melhor te atenda ou te divirta (depende do seu objetivo.) O meu é diversão.  Utilizar a API da Marvel trás alguns desafios que acho interessante.&#x20;

* precisamos de uma conta
* precisamos de um apiKey
* precisamos lidar com lentidão da requisição

Isso vai deixar nosso sensor aranha mais afiado.&#x20;

Vamos seguir alguns passos para ter acesso a API


# Criando uma conta

A primeira parte é acessar o [portal de desenvolvedor da Marvel](https://developer.marvel.com/). Você pode clicar em GET STARTED e acessar com sua conta Disney+. Ou acessar o [link de cadastro](https://developer.marvel.com/signup) diretamente.

<figure><img src="/files/e61U6FwylOZXpHWdPw97" alt=""><figcaption><p>Portal de desenvolvedor Marvel</p></figcaption></figure>

Feito isso você já está apto a utilizar a API.&#x20;


# Lendo o endpoint de Comics

A Marvel API nos disponibiliza alguns recursos legais, como obter personagens, historias em quadrinho, eventos, séries, entre outros.

Para ver todos os *endpoints*, só acessar o [link da documentação](https://developer.marvel.com/docs) e teremos ali uma lista completa dos recursos e como devemos montar nossas requisições.&#x20;

Em nosso estudo, utilizarei a listagem de Comics (revistas em quadrinhos)

<figure><img src="/files/JYJws4KlyBqbtGRvrUaU" alt=""><figcaption><p><a href="https://developer.marvel.com/docs#!/public/getComicsCollection_get_6">https://developer.marvel.com/docs#!/public/getComicsCollection_get_6</a></p></figcaption></figure>

Vamos extrair as informações para conseguir realizar a requisição igual como fizemos em nosso [serviço de cafés quentes](/construindo-um-cliente-usando-tesla/criando-o-client).

Olhando apenas o básico temos essa estrutura

```
GET https://<url_base>/v1/public/comics
```

Temos aqui&#x20;

| **Verbo**    | GET               |   |
| ------------ | ----------------- | - |
| **Recurso**  | /v1/public/comics |   |
| **URL base** | ?                 |   |

Mas esta faltando a URL base de nossa requisição. Para essa informação, podemos acessar o [General API Information](https://developer.marvel.com/documentation/getting_started). e veremos que nossa base é `https://gateway.marvel.com/`

| **Verbo**    | GET                          |
| ------------ | ---------------------------- |
| **Recurso**  | /v1/public/comics            |
| **URL base** | <https://gateway.marvel.com> |

Podemos agora montar nossa requisição e testala com o Tesla.

```
GET https://gateway.marvel.com/v1/public/comics
```

Convertendo

```elixir
Tesla.get("https://gateway.marvel.com/v1/public/comics")
```

Podemos acessar nosso terminal iterativo e testar a requisição

```
iex -S mix   
```

```elixir
Tesla.get("https://gateway.marvel.com/v1/public/comics")
```

```elixir
iex(1)> Tesla.get("https://gateway.marvel.com/v1/public/comics")
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://gateway.marvel.com/v1/public/comics",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 08 May 2024 17:57:08 GMT"},
     {"content-length", "68"},
     {"content-type", "application/json; charset=utf-8"}
   ],
   body: "{\"code\":\"MissingParameter\",\"message\":\"You must provide a user key.\"}",
   status: 409,
   opts: [],
   __module__: Tesla,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
```

Aqui temos nosso primeiro problema. Mesmo a API sendo publica, temos controle de quem está acessando. Nesse caso ele avisa que precisamos de um chave de usuário. E foi para isso que criamos uma conta na Marvel.

[Acesse sua conta de desenvolvedor](https://developer.marvel.com/account) e iremos encontrar a chave necessária. Sua API Key é sua chave pública. Marquei de amarelo.&#x20;

<figure><img src="/files/oHY10jNqZ11T9GRJnmCE" alt=""><figcaption><p><a href="https://developer.marvel.com/account">https://developer.marvel.com/account</a></p></figcaption></figure>

Vou nomear a minha como 12345, para facilitar o entendimento.

| **Verbo**    | GET                          |
| ------------ | ---------------------------- |
| **Recurso**  | /v1/public/comics            |
| **URL base** | <https://gateway.marvel.com> |
| A**pi Key**  | 12345                        |

Vamos atualizar nossa chamada com o Tesla. Para passar esse dado, utilizaremos query params.

{% code fullWidth="false" %}

```elixir
Tesla.get("https://gateway.marvel.com/v1/public/comics?apikey=12345")
```

{% endcode %}

<pre class="language-elixir"><code class="lang-elixir">iex(3)> Tesla.get("https://gateway.marvel.com/v1/public/comics?apikey=d3f934e3c9ccb6de467674e4fc7a3ead")
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://gateway.marvel.com/v1/public/comics?apikey=d3f934e3c9ccb6de467674e4fc7a3ead",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 08 May 2024 18:06:59 GMT"},
     {"content-length", "64"},
     {"content-type", "application/json; charset=utf-8"}
   ],
<strong>   body: "{\"code\":\"MissingParameter\",\"message\":\"You must provide a hash.\"}",
</strong>   status: 409,
   opts: [],
   __module__: Tesla,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
</code></pre>

Passamos do problema da *user key*. Mas caímos agora no problema do *hash*.

O *hash* na API da Marvel é uma encriptação simples usando MD5 que utiliza os dados

| Nome          | Descrição                                                                                                    |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| TS            | Uma *string* que servirá para termos unicidade nas requisições. Cada requisição deve ter um valor diferente. |
| Chave Publica | Que chamamos acima de *api key*                                                                              |
| Chave privada | Uma chave extra para realizar a *decrypt*. Basicamente, a chave para abrir.                                  |

As chaves encontramos na [conta de de desenvolvedor](https://developer.marvel.com/account).

Marquei em amarelo a <mark style="background-color:yellow;">publica</mark> e rosa a <mark style="background-color:red;">privada</mark>.

<figure><img src="/files/aqTiWyRbm6DovZPOnarY" alt=""><figcaption></figcaption></figure>

Utilizaremos como TS, por hora, o numero 1. Simplesmente para seguir com os estudos.

Como chave privada, demonstrarei como 88888.

A regra para o hash é:

```
<TS>+<chave_privada>+<chave_publica>
```

Podemos criar isso pelo elixir, entrando no modo iterativo

```elixir
:crypto.hash(:md5, "11234588888") |> Base.encode16()
```

```elixir
iex(1)> :crypto.hash(:md5, "11234588888") |> Base.encode16()
"7EE341DFFE88E697114117AA51E1A210"
```

Vamos agora pegar esse valor e adicionar a nossa query param. Também devemos colocar o valor de TS em nosso query o mesmo informado na criação do hash.

{% code fullWidth="false" %}

```elixir
Tesla.get("https://gateway.marvel.com/v1/public/comics?apikey=12345&hash=7EE341DFFE88E697114117AA51E1A210&ts=1")
```

{% endcode %}

<pre class="language-elixir" data-full-width="false"><code class="lang-elixir">iex(1)> Tesla.get("https://gateway.marvel.com/v1/public/comics?apikey=12345&#x26;hash=7EE341DFFE88E697114117AA51E1A210&#x26;ts=1")
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://gateway.marvel.com/v1/public/comics?apikey=d3f934e3c9ccb6de467674e4fc7a3ead&#x26;ts=1&#x26;hash=1d49870a0631827587878fcff98fa267",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 08 May 2024 18:21:51 GMT"},
     {"etag", "60ddd4a67c6e3242fa2560dbb0035b3925d666ef"},
     {"content-length", "66553"},
     {"content-type", "application/json; charset=utf-8"}
   ],
<strong>   body: "{\"code\":200,\"status\":\"Ok\",\"copyright\":\"© 2024 MARVEL\",\"attributionText\":\"Data provided by Marvel. © 2024 MARVEL\",\"attributionHTML\":\"&#x3C;a href=\\\"http://marvel.com\\\">Data provided by Marvel. © 2024 MARVEL&#x3C;/a>\",\"etag\":\"60ddd4a67c6e3242fa2560dbb0035b3925d666ef\",\"data\":{\"offset\":0,\"limit\":20,\"total\":60239,\"count\":20,\"results\":[{\"id\":82967,\"digitalId\":0,\"title\":\"Marvel Previews (2017)\",\"issueNumber\":0,\"variantDescription\":\"\",\"description\":\"\",\"modified\":\"2019-11-07T08:46:15-0500\",\"isbn\":\"\",\"upc\":\"75960608839302811\",\"diamondCode\":\"\",\"ean\":\"\",\"issn\":\"\",\"format\":\"\",\"pageCount\":112,\"textObjects\":[],\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82967\",\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/comics/issue/82967/marvel_previews_2017?utm_campaign=apiRef&#x26;utm_source=25a07f7adccf7328d3153451c26bd992\"}],\"series\":{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/23665\",\"name\":\"Marvel Previews (2017 - Present)\"},\"variants\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82965\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82970\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82969\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/74697\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/72736\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/75668\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65364\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65158\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65028\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/75662\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/74320\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/73776\",\"name\":\"Marvel Previews (2017)\"}],\"collections\":[],\"collectedIssues\":[],\"dates\":[{\"type\":\"onsaleDate\",\"date\":\"2099-10-30T00:00:00-0500\"},{\"type\":\"focDate\",\"date\":\"2019-10-07T00:00:00-0400\"}],\"prices\":[{\"type\":\"printPrice\",\"price\":0}],\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"images\":[],\"creators\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/creators\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/creators/10021\",\"name\":\"Jim Nausedas\",\"role\":\"editor\"}],\"returned\":1},\"characters\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/characters\",\"items\":[],\"returned\":0},\"stories\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/183698\",\"name\":\"cover from Marvel Previews (2017)\",\"type\":\"cover\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/183699\",\"name\":\"story from Marvel Previews (2017)\",\"type\":\"interiorStory\"}],\"returned\":2},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/events\",\"items\":[],\"returned\":0}},{\"id\":82965,\"digitalId\":0,\"title\":\"Marvel Previews (2017)\",\"issueNumber\":0,\"variantDescription\":\"\",\"description\":\"\",\"modified\":\"2019-08-21T17:11:27-0400\",\"isbn\":\"\",\"upc\":\"75960608839302611\",\"diamondCode\":\"JUL190068\",\"ean\":\"\",\"issn\":\"\",\"format\":\"\",\"pageCount\":152,\"textObjects\":[],\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82965\",\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/comics/issue/82965/marvel_previews_2017?utm_campaign=apiRef&#x26;utm_source=25a07f7adccf7328d3153451c26bd992\"}],\"series\":{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/23665\",\"name\":\"Marvel Previews (2017 - Present)\"},\"variants\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82967\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.co" &#x3C;> ...,
</strong>   status: 200,
   opts: [],
   __module__: Tesla,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
</code></pre>

Pronto, estamos com todo acesso necessário para utilizar a API da Marvel. Bora escrever nosso cliente.&#x20;


# Criando o cliente da Marvel

Vamos lá mais uma vez. Para termos a conexão com um serviço, precisamos criar essa conexão. Aqui estamos chamando ele de Client e vivera no contexto de integrações. Criaremos o arquivo `lib/coffee_shop/integrations/marvel_comics/client.ex` utilizando o mesmo padrão do cliente do café já criado.&#x20;

Iremos decompor essa chamada do tesla para dentro de nosso novo modulo utilizando os padrões estabelecidos nos capitulos anteriores

```elixir
Tesla.get("https://gateway.marvel.com/v1/public/comics?apikey=12345&hash=7EE341DFFE88E697114117AA51E1A210&ts=1")
```

* Utilizaremos uma função privada para configurar nosso cliente Tesla de forma práticas para os testes.
* Colocaremos a url base em um função isolada, decidindo chamar ela de `https://gateway.marvel.com/v1/public` uma vez que utilizaremos sempre essa base.
* Criaremos uma função publica que fara a requisição

{% code title="lib/coffee\_shop/integrations/marvel\_comics/client.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.MarvelComics.Client do
   def comics(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/v1/public/comics")
  end

  defp new_client(_opts) do
    middlewares = []

    Tesla.client(middlewares)
  end

  defp base_url() do
    "https://gateway.marvel.com"
  end
end

```

{% endcode %}

Essa é a cara base. Precisamos de uma teste para ele.

<pre class="language-elixir" data-title="test/coffee_shop/integrations/marvel_comics/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.MarvelComics.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.MarvelComics.Client

  setup do
    bypass = Bypass.open(port: 3000)
    {:ok, bypass: bypass}
  end

  describe "comics/0" do
    test "respond a list of comics", %{bypass: bypass} do
      response = ""

      Bypass.expect_once(bypass, "GET", "/v1/public/comics", fn conn ->
        assert %{
<strong>          "apiKey" => _,
</strong><strong>          "hash" => _,
</strong><strong>          "ts" => _
</strong>        } = conn.query_params

        Plug.Conn.resp(conn, 200, response)
      end)

      opts = [
        base_url: "http://localhost:3000"
      ]

      assert {:ok, response} = Client.comics(opts)
      assert response.status == 200
    end
  end
end

</code></pre>

Nesse teste, queremos garantir que a requisição chamada é do recurso que queremos. Também queremos garantir que os parâmetros *hash*, *api\_key* e *ts* estão sendo enviados juntos.

```
mix test test/coffee_shop/integrations/marvel_comics/client_test.exs
```

<pre class="language-elixir"><code class="lang-elixir">> mix test test/coffee_shop/integrations/marvel_comics/client_test.exs                                                                                                                                                                main [df2d2e3] modified untracked
Compiling 1 file (.ex)
....
16:33:14.917 [error] #PID&#x3C;0.407.0> running Bypass.Plug (connection #PID&#x3C;0.406.0>, stream id 1) terminated
Server: localhost:3000 (http)
Request: GET /v1/public/comics
** (exit) an exception was raised:
    ** (ExUnit.AssertionError) 

match (=) failed
code:  assert %{"apiKey" => _, "hash" => _, "ts" => _} = conn.query_params
<strong>left:  %{"apiKey" => _, "hash" => _, "ts" => _}
</strong><strong>right: %{}
</strong>
        test/coffee_shop/integrations/marvel_comics/client_test.exs:16: anonymous fn/1 in CoffeeShop.Integrations.MarvelComics.ClientTest."test comics/0 respond a list of comics"/1
        (bypass 2.1.0) lib/bypass/plug.ex:14: Bypass.Plug.call/2
        (plug_cowboy 2.7.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.12.0) /root/code/study/api/coffee_shop/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.12.0) /root/code/study/api/coffee_shop/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.12.0) /root/code/study/api/coffee_shop/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 5.0.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3


  1) test comics/0 respond a list of comics (CoffeeShop.Integrations.MarvelComics.ClientTest)
     test/coffee_shop/integrations/marvel_comics/client_test.exs:12
     Assertion with == failed
     code:  assert response.status == 200
     left:  500
     right: 200
     stacktrace:
       test/coffee_shop/integrations/marvel_comics/client_test.exs:30: (test)
</code></pre>

Como vimos na leitura do endpoint, precisamos enviar os três parâmetros para conseguir realizar a chamada. Vamos adiciona-los.

Para adicionar `query_params`, *Tesla* possui um [middleware ](https://hexdocs.pm/tesla/Tesla.Middleware.Query.html)que facilita o trabalho para nós. Vamos usa-lo.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/marvel_comics/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.MarvelComics.Client do
  def comics(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/v1/public/comics")
  end

  defp new_client(_opts) do
    middlewares = [
<strong>      {Tesla.Middleware.Query, [apikey: "12345", hash: "7EE341DFFE88E697114117AA51E1A210", ts: 1]}
</strong>    ]

    Tesla.client(middlewares)
  end

  defp base_url() do
    "https://gateway.marvel.com"
  end
end

</code></pre>

Decidimos seguir o mesmo padrão adotado no cliente do café e tiramos proveito disso. Basta adicionar o middleware direto na criação do *Tesla.client*. Com isso, podemos rodar os teses novamente.

```
mix test test/coffee_shop/integrations/marvel_comics/client_test.exs
```

```elixir
> mix test test/coffee_shop/integrations/marvel_comics/client_test.exs                                                                                                                         main [df2d2e3] modified untracked
.
Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 test, 0 failures
```

Com isso garantimos que estamos passando todos os dados que precisamos para realizar a requisição.&#x20;


# Melhorando a segurança

Conseguimos nos conectar, mas ferimos algumas diretrizes de segurança. Temos chaves privadas e publicas expostas em nosso código, isso não é um bom sinal. Precisamos remove-los.

{% hint style="info" %}
Nesse caso, você precisa de algum serviço que gerencie essas chaves. Nesse livro não abordaremos isso, então a extração estará em nível de arquivos de configuração. Caso queira, você pode completar o código utilizando algum gerenciador e removendo completamente a chave do projeto.
{% endhint %}

Não iremos remover totalmente de nosso projeto de estudo, por ser um projeto de estudo. Mas você como desenvolvedor, deve ter ciencia que isso é um risco e deve ser resolvido arrancando completamente de seu código.&#x20;

Dito isso, vamos apenas mover nossa chave para nível de configuração de variável de ambiente com o default sendo utilizado a chave. Como falei, isso não deve ir para um repositorio. Está assim apenas por fins de estudos.


# Lidando com a resposta

Conseguimos faze a requisição para a API da Marvel. O *endpoint* que batemos traz uma lista de Quadrinhos. Essa foi a resposta que obtemos:

```elixir
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://gateway.marvel.com/v1/public/comics?apikey=d3f934e3c9ccb6de467674e4fc7a3ead&ts=1&hash=1d49870a0631827587878fcff98fa267",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 08 May 2024 18:21:51 GMT"},
     {"etag", "60ddd4a67c6e3242fa2560dbb0035b3925d666ef"},
     {"content-length", "66553"},
     {"content-type", "application/json; charset=utf-8"}
   ],
   body: "{\"code\":200,\"status\":\"Ok\",\"copyright\":\"© 2024 MARVEL\",\"attributionText\":\"Data provided by Marvel. © 2024 MARVEL\",\"attributionHTML\":\"<a href=\\\"http://marvel.com\\\">Data provided by Marvel. © 2024 MARVEL</a>\",\"etag\":\"60ddd4a67c6e3242fa2560dbb0035b3925d666ef\",\"data\":{\"offset\":0,\"limit\":20,\"total\":60239,\"count\":20,\"results\":[{\"id\":82967,\"digitalId\":0,\"title\":\"Marvel Previews (2017)\",\"issueNumber\":0,\"variantDescription\":\"\",\"description\":\"\",\"modified\":\"2019-11-07T08:46:15-0500\",\"isbn\":\"\",\"upc\":\"75960608839302811\",\"diamondCode\":\"\",\"ean\":\"\",\"issn\":\"\",\"format\":\"\",\"pageCount\":112,\"textObjects\":[],\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82967\",\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/comics/issue/82967/marvel_previews_2017?utm_campaign=apiRef&utm_source=25a07f7adccf7328d3153451c26bd992\"}],\"series\":{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/23665\",\"name\":\"Marvel Previews (2017 - Present)\"},\"variants\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82965\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82970\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82969\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/74697\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/72736\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/75668\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65364\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65158\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/65028\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/75662\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/74320\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/73776\",\"name\":\"Marvel Previews (2017)\"}],\"collections\":[],\"collectedIssues\":[],\"dates\":[{\"type\":\"onsaleDate\",\"date\":\"2099-10-30T00:00:00-0500\"},{\"type\":\"focDate\",\"date\":\"2019-10-07T00:00:00-0400\"}],\"prices\":[{\"type\":\"printPrice\",\"price\":0}],\"thumbnail\":{\"path\":\"http://i.annihil.us/u/prod/marvel/i/mg/b/40/image_not_available\",\"extension\":\"jpg\"},\"images\":[],\"creators\":{\"available\":1,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/creators\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/creators/10021\",\"name\":\"Jim Nausedas\",\"role\":\"editor\"}],\"returned\":1},\"characters\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/characters\",\"items\":[],\"returned\":0},\"stories\":{\"available\":2,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/stories\",\"items\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/183698\",\"name\":\"cover from Marvel Previews (2017)\",\"type\":\"cover\"},{\"resourceURI\":\"http://gateway.marvel.com/v1/public/stories/183699\",\"name\":\"story from Marvel Previews (2017)\",\"type\":\"interiorStory\"}],\"returned\":2},\"events\":{\"available\":0,\"collectionURI\":\"http://gateway.marvel.com/v1/public/comics/82967/events\",\"items\":[],\"returned\":0}},{\"id\":82965,\"digitalId\":0,\"title\":\"Marvel Previews (2017)\",\"issueNumber\":0,\"variantDescription\":\"\",\"description\":\"\",\"modified\":\"2019-08-21T17:11:27-0400\",\"isbn\":\"\",\"upc\":\"75960608839302611\",\"diamondCode\":\"JUL190068\",\"ean\":\"\",\"issn\":\"\",\"format\":\"\",\"pageCount\":152,\"textObjects\":[],\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82965\",\"urls\":[{\"type\":\"detail\",\"url\":\"http://marvel.com/comics/issue/82965/marvel_previews_2017?utm_campaign=apiRef&utm_source=25a07f7adccf7328d3153451c26bd992\"}],\"series\":{\"resourceURI\":\"http://gateway.marvel.com/v1/public/series/23665\",\"name\":\"Marvel Previews (2017 - Present)\"},\"variants\":[{\"resourceURI\":\"http://gateway.marvel.com/v1/public/comics/82967\",\"name\":\"Marvel Previews (2017)\"},{\"resourceURI\":\"http://gateway.marvel.co" <> ...,
   status: 200,
   opts: [],
   __module__: Tesla,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}
```

A primeira coisa que gosto de fazer, é tirar a resposabilidade da resposta do Tesla e criar um Response nosso. Vamos fazer isso.

{% code title="lib/coffee\_shop/integrations/marvel\_comics/response.ex" lineNumbers="true" %}

```elixir
defmodule CoffeeShop.Integrations.MarvelComics.Response do
  defstruct status: :integer, body: :map

  def build({:ok, %Tesla.Env{status: status, body: body}}) do
    response = %__MODULE__{
      status: status,
      body: body
    }

    {:ok, response}
  end
end

```

{% endcode %}

Ele segue a mesma estrutura do respose do Café. Deixo separado para facilitar tratamentos de resposta para cada integração. Não faz sentido termos a mesma resposta em várias integrações e logo veremos isso. Vamos adicionar em nosso cliente.

<pre class="language-elixir" data-title="lib/coffee_shop/integrations/marvel_comics/client.ex" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.MarvelComics.Client do
<strong>  alias CoffeeShop.Integrations.MarvelComics.Response
</strong>  
  def comics(opts \\ []) do
    base_url = Keyword.get(opts, :base_url, base_url())

    opts
    |> new_client()
    |> Tesla.get("#{base_url}/v1/public/comics")
<strong>    |> Response.build()
</strong>  end

  defp new_client(_opts) do
    middlewares = [
      {Tesla.Middleware.Query, [apiKey: "12345", hash: "7EE341DFFE88E697114117AA51E1A210", ts: 1]}
    ]

    Tesla.client(middlewares)
  end

  defp base_url() do
    "https://gateway.marvel.com"
  end
end

</code></pre>

Precisamos atualizar nosso teste

<pre class="language-elixir" data-title="test/coffee_shop/integrations/marvel_comics/client_test.exs" data-line-numbers><code class="lang-elixir">defmodule CoffeeShop.Integrations.MarvelComics.ClientTest do
  use ExUnit.Case

  describe "comics/0" do
    test "respond a list of comics", %{bypass: bypass} do
      # ...
      
<strong>      assert {:ok, %Response{status: 200, body: _}} = Client.comics(opts)
</strong>    end
  end
end

</code></pre>

```
mix test test/coffee_shop/integrations/marvel_comics/client_test.exs
```

```elixir
> mix test test/coffee_shop/integrations/marvel_comics/client_test.exs
Compiling 1 file (.ex)
.
Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 test, 0 failures
```


# Aproveitando ao máximo o Rate Limit

Mesmo tendo mecanismos para lidar com o *Rate Limit* ultrapassado, precisamos também aproveitar ao máximo as requisições e tentar não entrar na penalidade, ou entrar o mínimo possível, para entregar uma experiência melhor ao usuário.

Uma das formas de fazer isso é utilizando *ETag*, um mecanismo de versionamento de recurso.

Hoje nossa aplicação funciona assim

<figure><img src="/files/TYDFFGzGEr5ludrM8EPy" alt=""><figcaption></figcaption></figure>

Vamos recapitular.

Nossa aplicação realiza uma requisição para o serviço externo requisitando o recurso `/coffee/hot`. O serviço externo processa, pega os dados de algum sistema de armazenamento e retorna o recurço. Na resposta recebemos um cabeçalho com  `x-ratelimit-remaining` . Toda vez que fizermos esse processo, esse cabeçalho diminuirá 1, até chegar a zero e então receberemos o erro de status 429 acusando que fizemos requisições demais em um curto período de tempo.

Imagine agora que o serviço externo versiona os dados do recurso. Isso quer dizer, toda vez que o dado muda, ou um cafezinho é adicionado, ele gera um código informando que essa é a versão atual e responde esse código pelo cabeçalho para nós, quem está requisitando o recurso. Vamos dizer que o código seja 123456. Chegou a hora que precisamos fazer novamente um requisição a esse recurso, pois alguém chamou a função de integração. Tendo o código da versão do recurso podemos mandar por cabeçalho. Feito isso, do o serviço analisara esse código, verificando se é a versão atual. Se não foi, ele irá retornar os dados do recurso, junto com o codigo mais recente. Caso o código que nós enviamos seja igual ao da versão atual, ele vai nos responder com status `304 Not Modified` e o corpo da requisição virá vazia. Isso quer dizer que o dado que temos é o atualizado.

<figure><img src="/files/7hNhPXzDXJDZ6rjFmQD8" alt=""><figcaption></figcaption></figure>

Nesse cenário o `x-ratelimit-remaining` não é decrementado. Isso acontece devido a não ter processado o dado no lado do serviço. Essa regra faz com que não consumemos muito recurso, com isso não nos penalizando.

Porém, não recebemos o dado na requisição. Precisamos ter ele em nossa aplicação em alguma forma de armazenamento. No nossa caso, podemos adicionar no banco de dados.


# WIP - Supervisor


# WIP - OAuth

Nesse livros utilizamos até então um acesso simples ao serviço. Basta mandar um requisição e você tem q resposta. Também vimos esse trafego usando um Token simples chumbado em nosso projeto.  Porém, e se nós precisarmos integrar em um API que não utiliza um mecanismo simples de token? E se precisarmos realizar uma requisição em nome de um usuário? Ou permitir que o usuário faça login em nosso aplicativo usando contas de terceiros, por exemplo,  Google, X ou Github.

Nessa unidade entenderemos melhor sobre *OAuth* para entendermos seu poder e no que pode nos ajudar a criar um serviço incrível.


# WIP - Cacheando requisições


