Protocolos de Red: TCP, Métodos HTTP y Códigos de Estado Comunes

En el desarrollo de software y las pruebas, comprender los mecanismos de comunicación subyacentes es crucial. Este artículo profundiza en varios aspectos de las interacciones de red, centrándose en el modelo de pruebas, el protocolo TCP, los métodos HTTP y los códigos de respuesta.

Modelo de Pruebas de Software

El modelo de prueba de software a menudo se visualiza como una pirámide con tres niveles principales: Unidad, Servicio y UI. Nos centraremos en la capa de Servicio. Las pruebas en este nivel validan las funiconalidades de las interfaces de programación de aplicaciones (API), asegurando que la lógica interna y el manejo de excepciones funcionen correctamente. El objetivo es verificar la estabilidad de las API. Las pruebas de API son relativamente fáciles de implementar, eficientes y tienen un bajo costo de mantenimiento de casos de prueba.

Protocolo TCP y el Proceso de "Tres Vías de Saludo"

Antes de que un cliente y un servidor puedan intercambiar datos, deben establecer una conexión TCP. Este proceso, conocido como el "saludo de tres vías", funciona de la siguiente manera:

  1. El cliente inicia una solicitud para establecer una conexión TCP enviando un paquete SYN.
  2. El servidor responde con un paquete SYN-ACK, reconociendo la solicitud del cliente y enviando su propia solicitud de sincronización.
  3. El cliente responde con un paquete ACK, confirmando la recepción del SYN-ACK del servidor, completando así el establecimiento de la conexión.

Una vez establecida la conexión, el flujo de comunicación típico implica:

  1. El cliente envía una solicitud HTTP al servidor, que incluye la URL solicitada, el método HTTP (por ejemplo, GET, POST), las cabeceras y los parámetros.
  2. El servidor procesa la solicitud y envía una respuesta HTTP al cliente. Esta respuesta contiene el código de estado del protcoolo, las cabeceras de respuesta y los datos de la respuesta.
  3. Finalmente, la conexión TCP se cierra, ya sea por el cliente o el servidor, enviando un paquete FIN.

Patrones de Comunicación

Tanto el cliente como el servidor deben existir para que la comunicación ocurra. Dos patrones primarios son:

  • Solicitudes Síncronas: El cliente envía una solicitud y espera una respuesta directa del servidor. Si el servidor tarda en responder (debido a cálculos intensivos o problemas lógicos), la solicitud del cliente puede expirar o bloquearse, lo que afecta la capacidad de respuesta.
  • Solicitudes Asíncronas: Para superar las limitaciones de tiempo de espera y bloqueo de las solicitudes síncronas, se emplean las solicitudes asíncronas. En este modelo, el cliente y el servidor no necesitan esperar activamente las respuestas del otro. En su lugar, la comunicación se facilita a través de un intermediario, como un sistema de colas de mensajes (MQ). Las entidades publican mensajes en la cola, y los otros consumen estos mensajes para continuar el procesamiento.

Métodos HTTP Comunes

  • GET: Se utiliza para recuperar datos de un recurso especificado. Es idempotente y seguro (no debe tener efectos secundarios).
  • POST: Se utiliza para enviar datos a un servidor para su procesamiento. A menudo se usa para crear nuevos recursos o enviar datos de formularios. Puede tener efectos secundarios.
  • PUT: Se utiliza para actualizar un recurso existente en el servidor con los datos proporcionados. Si el recurso no existe, a veces se crea. Es idempotente.
  • DELETE: Se utiliza para solicitar la eliminación de un recurso especificado en el servidor. Es idempotente.

Códigos de Estado HTTP Comunes

Los códigos de estado indican el resultado de una solicitud HTTP:

  • 2xx (Éxito):
    • 200 OK: La solicitud se ha completado con éxito.
  • 3xx (Redirección):
    • 301 Moved Permanently: El recurso solicitado ha sido movido permanentemente a una nueva URL.
    • 302 Found: El recurso solicitado se encuentra temporalmente en una URL diferente.
  • 4xx (Error del Cliente): Estos errores indican un problema con la solicitud enviada por el cliente.
    • 400 Bad Request: La solicitud del cliente es incorrecta o mal formada (por ejemplo, encabezados o parámetros inválidos).
    • 401 Unauthorized: La autenticación es necesaria y ha fallado o no se ha proporcionado.
    • 403 Forbidden: El servidor entiende la solicitud, pero se niega a autorizarla.
    • 404 Not Found: El recurso solicitado no se pudo encontrar en el servidor.
    • 405 Method Not Allowed: El método HTTP utilizado en la solicitud no está permitido para el recurso solicitado.
  • 5xx (Error del Servidor): Estos errores indican que el servidor falló al procesar una solicitud válida.
    • 500 Internal Server Error: El servidor encontró una condición inesperada que le impidió cumplir con la solicitud.
    • 504 Gateway Timeout: El servidor actuó como puerta de enlace o proxy y no recibió una respuesta oportuna del servidor de origen.

Cabeceras HTTP (Solicitud y Respuesta)

Las cabeceras HTTP proporcionan metadatos sobre la solicitud o la respuesta:

  • Cookie: Envía datos de cookies almacenados previamente por el servidor al cliente, utilizados para mantener el estado de la sesión.
  • Referer: Indica la URL de la página de la que proviene la solicitud actual.
  • User-Agent: Identifica el software cliente (navegador, sistema operativo) que realiza la solicitud.
  • Content-Type: Especifica el tipo de medio de los datos que se envían en el cuerpo de la solicitud o respuesta (por ejemplo, application/json).
  • Content-Encoding: Indica la codificación aplicada al cuerpo de la respuesta (por ejemplo, gzip).
  • Server: Proporciona información sobre el software del servidor web.
  • Set-Cookie: Utilizado en las respuestas del servidor para instruir al cliente a almacenar una cookie.

Formatos de Datos de Solicitud Comunes

Los datos enviados en las solicitudes HTTP pueden adoptar varios formatos:

  • application/x-www-form-urlencoded: El formato predeterminado para los envíos de formularios HTML, donde los pares clave-valor se codifican como URL.
  • multipart/form-data: Utilizado para la carga de archivos y el envío de datos de formulario mixtos.
  • application/json: Los datos se envían en formato JSON (JavaScript Object Notation), que es ampliamente utilizado para las API web.
  • text/xml: Los datos se estructuran en formato XML (Extensible Markup Language).

Ejemplo Práctico: Implementación de una API de Inicio de Sesión con Flask y Pruebas con requests

A continuación, se muestra un ejemplo de cómo crear un punto final de inicio de sesión simple usando Flask y cómo probarlo con la biblioteca requests de Python.

login_api.py (Servidor Flask)


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, jsonify
from flask_restful import Api, Resource, reqparse

app = Flask(__name__)
api = Api(app)

class LoginResource(Resource):
   def get(self):
       # Respuesta GET simple para verificar la disponibilidad del endpoint
       return {'status': 0, 'msg': 'Servicio de login activo', 'data': 'Página de inicio de sesión'}

   def post(self):
       # Configuración del analizador para los argumentos esperados en la solicitud POST
       parser = reqparse.RequestParser()
       parser.add_argument('username', type=str, required=True, help='El nombre de usuario es obligatorio')
       parser.add_argument('password', type=str, required=True, help='La contraseña es obligatoria')
       parser.add_argument('age', type=int, help='La edad debe ser un número entero')
       parser.add_argument('sex', type=str, choices=['Mujer', 'Hombre'], help='El género debe ser "Mujer" u "Hombre"')
       
       try:
           args = parser.parse_args(strict=True) # strict=True lanzará un error para argumentos no esperados
           # En una aplicación real, aquí iría la lógica de autenticación
           return jsonify(args) # Devuelve los argumentos analizados como confirmación
       except Exception as e:
           # Devuelve un error 400 Bad Request si falta un argumento requerido o hay un error de tipo
           # Flask-RESTful maneja automáticamente esto y devuelve un mensaje de error apropiado.
           # Para una personalización más profunda, se podría capturar reqparse.exceptions.ArgumentError
           # y devolver un código de estado y mensaje personalizado.
           # Por simplicidad, devolvemos el jsonify de los args parseados si tiene éxito,
           # y dejamos que Flask-RESTful maneje los errores de análisis.
           # Si el análisis falla, Flask-RESTful devuelve un 400 por defecto.
           # El mensaje de error específico se incluirá en la respuesta JSON por defecto.
           pass # Dejar que Flask-RESTful maneje el error de análisis

api.add_resource(LoginResource, '/login', endpoint='login')

if __name__ == '__main__':
   # Ejecutar el servidor Flask en modo debug en el puerto 5000
   app.run(debug=True, port=5000)
 

test_api.py (Cliente de Pruebas)


import requests
import unittest
import json

# Nota: Las solicitudes GET y POST comentadas son ejemplos de cómo usar la biblioteca `requests` directamente.
# El bloque principal utiliza la estructura de `unittest` para pruebas más robustas.

# # Ejemplo de solicitud GET
# r = requests.get(url='http://localhost:5000/login')
# print('Código de Estado:', r.status_code)
# print('Cabeceras de Respuesta:', r.headers)
# if r.headers.get('Content-Type') == 'application/json':
#     print('Datos de Respuesta (JSON):', r.json())
# print('Contenido de Respuesta (Texto):', r.text)
# print('Contenido Binario:', r.content)
# print('Tiempo de Respuesta (segundos):', r.elapsed.total_seconds())
# print('Cookies:', r.cookies)

# # Ejemplo de solicitud POST
# # Usando el parámetro 'json' que automáticamente serializa a JSON y establece 'Content-Type' a 'application/json'
# r = requests.post(
#     url='http://localhost:5000/login',
#     json={'username': 'testuser', 'password': 'password123', 'age': 30, 'sex': 'Mujer'}
# )
# print('Código de Estado POST:', r.status_code)
# print('Respuesta JSON POST:', r.json())


class APITests(unittest.TestCase):
   def setUp(self):
       # Configuración común para todas las pruebas
       self.base_url = 'http://localhost:5000/login'
       self.valid_payload = {'username': 'testuser', 'password': 'password123', 'age': 30, 'sex': 'Mujer'}
       self.json_headers = {'Content-Type': 'application/json'}

   def test_login_get_success(self):
       '''Prueba la solicitud GET al endpoint de login.'''
       response = requests.get(url=self.base_url)
       self.assertEqual(response.status_code, 200)
       response_data = response.json()
       self.assertEqual(response_data.get('status'), 0)
       self.assertEqual(response_data.get('data'), 'Página de inicio de sesión')

   def test_login_post_success(self):
       '''Prueba una solicitud POST válida al endpoint de login.'''
       response = requests.post(
           url=self.base_url,
           json=self.valid_payload,
           headers=self.json_headers
       )
       self.assertEqual(response.status_code, 200)
       response_data = response.json()
       self.assertEqual(response_data.get('username'), 'testuser')
       self.assertEqual(response_data.get('age'), 30)
       self.assertEqual(response_data.get('sex'), 'Mujer')

   def test_login_post_missing_username(self):
       '''Prueba el manejo de una solicitud POST sin el campo 'username'.'''
       payload = self.valid_payload.copy()
       del payload['username']
       response = requests.post(
           url=self.base_url,
           json=payload,
           headers=self.json_headers
       )
       self.assertEqual(response.status_code, 400)
       # Flask-RESTful por defecto devuelve un mensaje de error en 'message'
       self.assertIn('username', response.json().get('message', {}))
       self.assertEqual(response.json()['message']['username'], 'El nombre de usuario es obligatorio')

   def test_login_post_missing_password(self):
       '''Prueba el manejo de una solicitud POST sin el campo 'password'.'''
       payload = self.valid_payload.copy()
       del payload['password']
       response = requests.post(
           url=self.base_url,
           json=payload,
           headers=self.json_headers
       )
       self.assertEqual(response.status_code, 400)
       self.assertIn('password', response.json().get('message', {}))
       self.assertEqual(response.json()['message']['password'], 'La contraseña es obligatoria')

   def test_login_post_invalid_age_type(self):
       '''Prueba el manejo de una solicitud POST con un tipo de dato incorrecto para 'age'.'''
       payload = self.valid_payload.copy()
       payload['age'] = "treinta" # Edad como string en lugar de entero
       response = requests.post(
           url=self.base_url,
           json=payload,
           headers=self.json_headers
       )
       self.assertEqual(response.status_code, 400)
       self.assertIn('age', response.json().get('message', {}))
       self.assertEqual(response.json()['message']['age'], 'La edad debe ser un número entero')
       
   def test_login_post_invalid_sex_choice(self):
       '''Prueba el manejo de una solicitud POST con un valor no permitido para 'sex'.'''
       payload = self.valid_payload.copy()
       payload['sex'] = "Otro" # Valor no permitido
       response = requests.post(
           url=self.base_url,
           json=payload,
           headers=self.json_headers
       )
       self.assertEqual(response.status_code, 400)
       self.assertIn('sex', response.json().get('message', {}))
       self.assertEqual(response.json()['message']['sex'], 'El género debe ser "Mujer" u "Hombre"')

   # Pruebas adicionales para un servicio externo (ejemplo de "cunYou")
   def test_external_service_search_one(self):
       '''Prueba una llamada a un servicio de búsqueda externo.'''
       search_url = 'http://m.cyw.com/Home/ComSearch/search'
       data_payload = {'k': 'termas de agua'}
       headers = {
           'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
           'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'
       }
       response = requests.post(url=search_url, data=data_payload, headers=headers)
       self.assertEqual(response.status_code, 200)
       # Asumiendo que la respuesta esperada para 'termas de agua' es 9 según el ejemplo original
       self.assertEqual(response.json().get('procedure'), 9)

   def test_external_service_search_two(self):
       '''Prueba otra llamada a un servicio de búsqueda externo con diferentes parámetros.'''
       search_url = 'http://m.cyw.com/Home/ComSearch/search'
       data_payload = {'k': 'barbacoa'}
       headers = {
           'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
           'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'
       }
       response = requests.post(url=search_url, data=data_payload, headers=headers)
       self.assertEqual(response.status_code, 200)
       # Asumiendo que la respuesta esperada para 'barbacoa' es 6 según el ejemplo original
       self.assertEqual(response.json().get('procedure'), 6)

if __name__ == '__main__':
   # Ejecutar todas las pruebas con verbosidad nivel 2
   unittest.main(verbosity=2)
 

Etiquetas: tcp HTTP API Flask Python

Publicado el 6-26 05:48