Skip to content
English
On this page

Ejercicio: Chat con WebSockets y Amazon Lex

Escenario

Implementaremos un chat que permita:

  • Comunicación en tiempo real con WebSockets
  • Integración con Amazon Lex para respuestas automáticas
  • Interfaz web simple
  • Persistencia de mensajes en DynamoDB
  • Autenticación con Cognito
  • Manejo de sesiones

Estructura del Proyecto

plaintext
chat-lex-app/
├── backend/
│   ├── functions/
│   │   ├── connect/
│   │   │   └── handler.js
│   │   ├── disconnect/
│   │   │   └── handler.js
│   │   ├── message/
│   │   │   └── handler.js
│   │   └── lexbot/
│   │       └── handler.js
│   │
│   ├── lib/
│   │   ├── dynamodb.js
│   │   ├── websocket.js
│   │   └── lex.js
│   │
│   └── models/
│       ├── Connection.js
│       └── Message.js

├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── Chat/
│   │   │   ├── MessageList/
│   │   │   └── UserInput/
│   │   │
│   │   ├── hooks/
│   │   │   ├── useWebSocket.js
│   │   │   └── useAuth.js
│   │   │
│   │   └── services/
│   │       └── api.js
│   │
│   ├── public/
│   │   └── index.html
│   │
│   └── package.json

├── infrastructure/
│   ├── cloudformation/
│   │   ├── api.yaml
│   │   ├── cognito.yaml
│   │   ├── dynamodb.yaml
│   │   └── lex.yaml
│   │
│   └── scripts/
│       ├── deploy.sh
│       └── setup.sh

├── test/
│   ├── integration/
│   └── unit/

├── package.json
└── README.md

Etapas del Ejercicio:

Etapa 1: Infraestructura Base

  • Configurar WebSocket API Gateway
  • Implementar DynamoDB
  • Configurar Cognito
  • Establecer roles IAM

Etapa 2: Backend Base

  • Implementar handlers de WebSocket
  • Configurar persistencia con DynamoDB
  • Manejar conexiones
  • Gestionar mensajes

Etapa 3: Integración con Lex

  • Configurar bot de Amazon Lex
  • Implementar intent handlers
  • Procesar respuestas automáticas
  • Manejar el contexto de la conversación

Etapa 4: Frontend Base

  • Implementar interfaz de usuario
  • Configurar WebSocket client
  • Manejar autenticación
  • Implementar chat UI

Etapa 5: Integraciones y Pruebas

  • Integrar backend y frontend
  • Implementar pruebas
  • Configurar logging
  • Manejar errores

Etapa 6: Optimización y Despliegue

  • Optimizar rendimiento
  • Configurar monitoreo
  • Implementar CI/CD
  • Documentar API

Etapa 1: Infraestructura Base

Objetivos:

  1. Configurar WebSocket API Gateway
  2. Implementar DynamoDB
  3. Configurar Cognito
  4. Establecer roles IAM

1.1 API Gateway WebSocket

yaml
# infrastructure/cloudformation/api.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'WebSocket API for Chat Application'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  WebSocketAPI:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Sub chat-api-${Environment}
      ProtocolType: WEBSOCKET
      RouteSelectionExpression: "$request.body.action"

  ConnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref WebSocketAPI
      RouteKey: $connect
      AuthorizationType: AWS_IAM
      OperationName: ConnectRoute
      Target: !Join
        - /
        - - integrations
          - !Ref ConnectIntegration

  DisconnectRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref WebSocketAPI
      RouteKey: $disconnect
      AuthorizationType: NONE
      OperationName: DisconnectRoute
      Target: !Join
        - /
        - - integrations
          - !Ref DisconnectIntegration

  MessageRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref WebSocketAPI
      RouteKey: sendmessage
      AuthorizationType: NONE
      OperationName: SendMessageRoute
      Target: !Join
        - /
        - - integrations
          - !Ref MessageIntegration

  ConnectIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref WebSocketAPI
      Description: Connect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri: 
        Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectFunction.Arn}/invocations

  DisconnectIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref WebSocketAPI
      Description: Disconnect Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectFunction.Arn}/invocations

  MessageIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref WebSocketAPI
      Description: Message Integration
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MessageFunction.Arn}/invocations

  Deployment:
    Type: AWS::ApiGatewayV2::Deployment
    DependsOn:
      - ConnectRoute
      - DisconnectRoute
      - MessageRoute
    Properties:
      ApiId: !Ref WebSocketAPI

  Stage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref WebSocketAPI
      DeploymentId: !Ref Deployment
      Name: !Ref Environment
      DefaultRouteSettings:
        DataTraceEnabled: true
        DetailedMetricsEnabled: true
        LoggingLevel: INFO

1.2 DynamoDB Tables

yaml
# infrastructure/cloudformation/dynamodb.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'DynamoDB Tables for Chat Application'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  ConnectionsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub chat-connections-${Environment}
      AttributeDefinitions:
        - AttributeName: connectionId
          AttributeType: S
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: connectionId
          KeyType: HASH
      GlobalSecondaryIndexes:
        - IndexName: UserIdIndex
          KeySchema:
            - AttributeName: userId
              KeyType: HASH
          Projection:
            ProjectionType: ALL
          ProvisionedThroughput:
            ReadCapacityUnits: !If [IsProd, 10, 5]
            WriteCapacityUnits: !If [IsProd, 10, 5]
      ProvisionedThroughput:
        ReadCapacityUnits: !If [IsProd, 10, 5]
        WriteCapacityUnits: !If [IsProd, 10, 5]
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true

  MessagesTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub chat-messages-${Environment}
      AttributeDefinitions:
        - AttributeName: conversationId
          AttributeType: S
        - AttributeName: timestamp
          AttributeType: N
        - AttributeName: userId
          AttributeType: S
      KeySchema:
        - AttributeName: conversationId
          KeyType: HASH
        - AttributeName: timestamp
          KeyType: RANGE
      GlobalSecondaryIndexes:
        - IndexName: UserMessageIndex
          KeySchema:
            - AttributeName: userId
              KeyType: HASH
            - AttributeName: timestamp
              KeyType: RANGE
          Projection:
            ProjectionType: ALL
          ProvisionedThroughput:
            ReadCapacityUnits: !If [IsProd, 10, 5]
            WriteCapacityUnits: !If [IsProd, 10, 5]
      ProvisionedThroughput:
        ReadCapacityUnits: !If [IsProd, 10, 5]
        WriteCapacityUnits: !If [IsProd, 10, 5]
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true

Conditions:
  IsProd: !Equals [!Ref Environment, prod]

Outputs:
  ConnectionsTableName:
    Value: !Ref ConnectionsTable
    Export:
      Name: !Sub ${AWS::StackName}-ConnectionsTableName

  MessagesTableName:
    Value: !Ref MessagesTable
    Export:
      Name: !Sub ${AWS::StackName}-MessagesTableName

1.3 Cognito Configuration

yaml
# infrastructure/cloudformation/cognito.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Cognito Configuration for Chat Application'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub chat-users-${Environment}
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
      AutoVerifiedAttributes:
        - email
      EmailVerificationMessage: "Your verification code is {####}"
      EmailVerificationSubject: "Chat App Verification Code"
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: true
          Required: true
        - Name: name
          AttributeDataType: String
          Mutable: true
          Required: true
      UserPoolAddOns:
        AdvancedSecurityMode: ENFORCED

  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: !Sub chat-client-${Environment}
      GenerateSecret: false
      ExplicitAuthFlows:
        - ALLOW_USER_SRP_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH
      PreventUserExistenceErrors: ENABLED
      SupportedIdentityProviders:
        - COGNITO
      AllowedOAuthFlows:
        - implicit
      AllowedOAuthScopes:
        - email
        - openid
        - profile
      CallbackURLs:
        - !Sub https://${Environment}.chatapp.com/callback
      LogoutURLs:
        - !Sub https://${Environment}.chatapp.com/logout

1.4 IAM Roles

yaml
# infrastructure/cloudformation/iam.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'IAM Roles for Chat Application'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  WebSocketLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub chat-websocket-lambda-${Environment}
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: WebSocketAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'execute-api:ManageConnections'
                Resource: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*'
              - Effect: Allow
                Action:
                  - 'dynamodb:PutItem'
                  - 'dynamodb:GetItem'
                  - 'dynamodb:DeleteItem'
                  - 'dynamodb:Query'
                  - 'dynamodb:Scan'
                Resource: 
                  - !GetAtt ConnectionsTable.Arn
                  - !GetAtt MessagesTable.Arn
              - Effect: Allow
                Action:
                  - 'lex:PostText'
                  - 'lex:PostContent'
                Resource: '*'

  ApiGatewayRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub chat-apigateway-${Environment}
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: InvokeLambda
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'lambda:InvokeFunction'
                Resource: '*'

Outputs:
  WebSocketLambdaRoleArn:
    Value: !GetAtt WebSocketLambdaRole.Arn
    Export:
      Name: !Sub ${AWS::StackName}-WebSocketLambdaRoleArn

  ApiGatewayRoleArn:
    Value: !GetAtt ApiGatewayRole.Arn
    Export:
      Name: !Sub ${AWS::StackName}-ApiGatewayRoleArn

Verificación de la Etapa 1

Checklist:

  • [ ] API Gateway WebSocket creado
  • [ ] Tablas DynamoDB creadas
  • [ ] User Pool Cognito configurado
  • [ ] Roles IAM establecidos
  • [ ] Permisos configurados
  • [ ] Endpoints disponibles
  • [ ] Logs habilitados

Pruebas Recomendadas:

  1. Verificar conexión WebSocket
  2. Probar tablas DynamoDB
  3. Validar autenticación Cognito
  4. Comprobar permisos IAM

Troubleshooting Común:

  1. Errores de API Gateway:

    • Verificar configuración de rutas
    • Revisar integraciones
    • Comprobar logs
  2. Problemas de DynamoDB:

    • Verificar índices
    • Revisar capacidad
    • Comprobar permisos
  3. Errores de Cognito:

    • Verificar configuración
    • Revisar políticas
    • Comprobar flujos
  4. Problemas de IAM:

    • Verificar roles
    • Revisar políticas
    • Comprobar permisos

Etapa 2: Backend Base

Objetivos:

  1. Implementar handlers de WebSocket
  2. Configurar persistencia con DynamoDB
  3. Manejar conexiones
  4. Gestionar mensajes

2.1 Manejadores de WebSocket

javascript
// backend/functions/connect/handler.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb');

const ddbClient = new DynamoDBClient();
const ddb = DynamoDBDocumentClient.from(ddbClient);

exports.handler = async (event) => {
    const connectionId = event.requestContext.connectionId;
    const timestamp = Date.now();

    try {
        // Verificar token de autenticación
        const auth = event.queryStringParameters?.Authorization;
        if (!auth) {
            return {
                statusCode: 401,
                body: 'Unauthorized'
            };
        }

        // Guardar conexión en DynamoDB
        await ddb.send(new PutCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            Item: {
                connectionId,
                userId: event.requestContext.authorizer.claims.sub,
                timestamp,
                ttl: Math.floor(timestamp / 1000) + 24 * 60 * 60, // 24 horas
                status: 'CONNECTED'
            }
        }));

        return {
            statusCode: 200,
            body: 'Connected'
        };
    } catch (err) {
        console.error('Error:', err);
        return {
            statusCode: 500,
            body: 'Failed to connect: ' + JSON.stringify(err)
        };
    }
};

// backend/functions/disconnect/handler.js
const { DeleteCommand } = require('@aws-sdk/lib-dynamodb');

exports.handler = async (event) => {
    const connectionId = event.requestContext.connectionId;

    try {
        await ddb.send(new DeleteCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            Key: {
                connectionId
            }
        }));

        return {
            statusCode: 200,
            body: 'Disconnected'
        };
    } catch (err) {
        console.error('Error:', err);
        return {
            statusCode: 500,
            body: 'Failed to disconnect: ' + JSON.stringify(err)
        };
    }
};

// backend/functions/message/handler.js
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');
const { PutCommand, QueryCommand } = require('@aws-sdk/lib-dynamodb');

exports.handler = async (event) => {
    const connectionId = event.requestContext.connectionId;
    const domain = event.requestContext.domainName;
    const stage = event.requestContext.stage;

    const apiGateway = new ApiGatewayManagementApiClient({
        endpoint: `https://${domain}/${stage}`
    });

    try {
        const body = JSON.parse(event.body);
        const message = {
            messageId: uuid.v4(),
            connectionId,
            userId: event.requestContext.authorizer.claims.sub,
            message: body.message,
            timestamp: Date.now(),
            type: 'USER_MESSAGE'
        };

        // Guardar mensaje en DynamoDB
        await ddb.send(new PutCommand({
            TableName: process.env.MESSAGES_TABLE,
            Item: message
        }));

        // Obtener conexiones activas
        const connections = await ddb.send(new QueryCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            IndexName: 'StatusIndex',
            KeyConditionExpression: '#status = :status',
            ExpressionAttributeNames: {
                '#status': 'status'
            },
            ExpressionAttributeValues: {
                ':status': 'CONNECTED'
            }
        }));

        // Enviar mensaje a todas las conexiones activas
        const sendMessages = connections.Items.map(async ({ connectionId }) => {
            try {
                await apiGateway.send(new PostToConnectionCommand({
                    ConnectionId: connectionId,
                    Data: JSON.stringify(message)
                }));
            } catch (err) {
                if (err.statusCode === 410) {
                    // Eliminar conexiones muertas
                    await ddb.send(new DeleteCommand({
                        TableName: process.env.CONNECTIONS_TABLE,
                        Key: { connectionId }
                    }));
                }
            }
        });

        await Promise.all(sendMessages);

        return {
            statusCode: 200,
            body: 'Message sent'
        };
    } catch (err) {
        console.error('Error:', err);
        return {
            statusCode: 500,
            body: 'Failed to send message: ' + JSON.stringify(err)
        };
    }
};

2.2 Capa de Persistencia

javascript
// backend/lib/dynamodb.js
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { 
    DynamoDBDocumentClient, 
    PutCommand, 
    GetCommand, 
    QueryCommand, 
    DeleteCommand 
} = require('@aws-sdk/lib-dynamodb');

class ChatRepository {
    constructor() {
        const ddbClient = new DynamoDBClient();
        this.ddb = DynamoDBDocumentClient.from(ddbClient);
    }

    async saveConnection(connectionData) {
        return await this.ddb.send(new PutCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            Item: {
                ...connectionData,
                ttl: Math.floor(Date.now() / 1000) + 24 * 60 * 60
            }
        }));
    }

    async removeConnection(connectionId) {
        return await this.ddb.send(new DeleteCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            Key: { connectionId }
        }));
    }

    async getActiveConnections() {
        const result = await this.ddb.send(new QueryCommand({
            TableName: process.env.CONNECTIONS_TABLE,
            IndexName: 'StatusIndex',
            KeyConditionExpression: '#status = :status',
            ExpressionAttributeNames: {
                '#status': 'status'
            },
            ExpressionAttributeValues: {
                ':status': 'CONNECTED'
            }
        }));
        return result.Items;
    }

    async saveMessage(messageData) {
        return await this.ddb.send(new PutCommand({
            TableName: process.env.MESSAGES_TABLE,
            Item: {
                ...messageData,
                ttl: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60 // 30 días
            }
        }));
    }

    async getMessageHistory(userId, limit = 50) {
        const result = await this.ddb.send(new QueryCommand({
            TableName: process.env.MESSAGES_TABLE,
            IndexName: 'UserMessageIndex',
            KeyConditionExpression: 'userId = :userId',
            ExpressionAttributeValues: {
                ':userId': userId
            },
            Limit: limit,
            ScanIndexForward: false
        }));
        return result.Items;
    }

    async getConversationMessages(conversationId, limit = 50) {
        const result = await this.ddb.send(new QueryCommand({
            TableName: process.env.MESSAGES_TABLE,
            KeyConditionExpression: 'conversationId = :conversationId',
            ExpressionAttributeValues: {
                ':conversationId': conversationId
            },
            Limit: limit,
            ScanIndexForward: false
        }));
        return result.Items;
    }
}

module.exports = ChatRepository;

// backend/lib/websocket.js
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require('@aws-sdk/client-apigatewaymanagementapi');

class WebSocketManager {
    constructor(domainName, stage) {
        this.apiGateway = new ApiGatewayManagementApiClient({
            endpoint: `https://${domainName}/${stage}`
        });
    }

    async broadcast(connections, message) {
        const sendPromises = connections.map(({ connectionId }) =>
            this.sendMessage(connectionId, message)
        );
        return Promise.all(sendPromises);
    }

    async sendMessage(connectionId, message) {
        try {
            await this.apiGateway.send(new PostToConnectionCommand({
                ConnectionId: connectionId,
                Data: JSON.stringify(message)
            }));
            return true;
        } catch (err) {
            if (err.statusCode === 410) {
                return { staleConnection: connectionId };
            }
            throw err;
        }
    }
}

module.exports = WebSocketManager;

2.3 Manejo de Mensajes

javascript
// backend/models/Message.js
class Message {
    constructor(data) {
        this.messageId = data.messageId;
        this.userId = data.userId;
        this.connectionId = data.connectionId;
        this.type = data.type;
        this.content = data.content;
        this.timestamp = data.timestamp || Date.now();
        this.metadata = data.metadata || {};
    }

    toJSON() {
        return {
            messageId: this.messageId,
            userId: this.userId,
            type: this.type,
            content: this.content,
            timestamp: this.timestamp,
            metadata: this.metadata
        };
    }

    static validateMessage(content) {
        if (!content || content.trim().length === 0) {
            throw new Error('Message content cannot be empty');
        }
        if (content.length > 1000) {
            throw new Error('Message content too long');
        }
        return true;
    }
}

// backend/models/Connection.js
class Connection {
    constructor(data) {
        this.connectionId = data.connectionId;
        this.userId = data.userId;
        this.timestamp = data.timestamp || Date.now();
        this.status = data.status || 'CONNECTED';
        this.metadata = data.metadata || {};
    }

    toJSON() {
        return {
            connectionId: this.connectionId,
            userId: this.userId,
            timestamp: this.timestamp,
            status: this.status,
            metadata: this.metadata
        };
    }
}

module.exports = {
    Message,
    Connection
};

Verificación de la Etapa 2

Checklist:

  • [ ] Handlers WebSocket implementados
  • [ ] Persistencia configurada
  • [ ] Manejo de conexiones funcional
  • [ ] Gestión de mensajes activa
  • [ ] Validaciones implementadas
  • [ ] Error handling configurado
  • [ ] Logging funcionando

Pruebas Recomendadas:

  1. Verificar conexión/desconexión
  2. Probar envío de mensajes
  3. Validar persistencia
  4. Comprobar broadcast

Troubleshooting Común:

  1. Problemas de WebSocket:

    • Verificar conexiones
    • Revisar timeouts
    • Comprobar permisos
  2. Errores de Persistencia:

    • Verificar transacciones
    • Revisar índices
    • Comprobar TTLs
  3. Problemas de Mensajes:

    • Verificar validaciones
    • Revisar broadcast
    • Comprobar orden
  4. Errores Generales:

    • Verificar logs
    • Revisar errores
    • Comprobar timeouts

Etapa 3: Integración con Lex

Objetivos:

  1. Configurar bot de Amazon Lex
  2. Implementar intent handlers
  3. Integrar con el chat
  4. Manejar respuestas del bot

3.1 Configuración de Amazon Lex

yaml
# infrastructure/cloudformation/lex.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Amazon Lex Configuration for Chat Bot'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  ChatBot:
    Type: AWS::Lex::Bot
    Properties:
      Name: !Sub chatbot-${Environment}
      DataPrivacy:
        ChildDirected: false
      IdleSessionTTLInSeconds: 300
      RoleArn: !GetAtt LexBotRole.Arn
      BotLocales:
        - LocaleId: "en_US"
          Description: "English bot for chat assistance"
          NluConfidenceThreshold: 0.40
          VoiceSettings:
            VoiceId: "Salli"
          Intents:
            - Name: "Greeting"
              Description: "Intent to handle greetings"
              SampleUtterances:
                - "hello"
                - "hi"
                - "hey"
                - "good morning"
                - "good evening"
              FulfillmentCodeHook:
                Enabled: true
            - Name: "Help"
              Description: "Intent to provide help"
              SampleUtterances:
                - "help"
                - "i need help"
                - "can you help me"
                - "what can you do"
              FulfillmentCodeHook:
                Enabled: true
            - Name: "Status"
              Description: "Intent to check status"
              SampleUtterances:
                - "status"
                - "what is my status"
                - "check status"
              FulfillmentCodeHook:
                Enabled: true
              Slots:
                - Name: "StatusType"
                  SlotTypeName: "StatusTypes"
                  ValueElicitationSetting:
                    SlotConstraint: "Required"
                    PromptSpecification:
                      MessageGroups:
                        - Message:
                            PlainTextMessage:
                              Value: "What type of status would you like to check?"
          SlotTypes:
            - Name: "StatusTypes"
              Description: "Types of status that can be checked"
              ValueSelectionSetting:
                ResolutionStrategy: "OriginalValue"
              SlotTypeValues:
                - SampleValue:
                    Value: "order"
                - SampleValue:
                    Value: "account"
                - SampleValue:
                    Value: "delivery"

  LexBotRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lexv2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLexV2BotPolicy

Outputs:
  BotId:
    Description: "The ID of the Lex bot"
    Value: !Ref ChatBot
    Export:
      Name: !Sub ${AWS::StackName}-BotId

3.2 Intent Handlers

javascript
// backend/functions/lexbot/handler.js
const { LexRuntimeV2Client, RecognizeTextCommand } = require("@aws-sdk/client-lex-runtime-v2");

class LexBotHandler {
    constructor() {
        this.lexClient = new LexRuntimeV2Client();
        this.botId = process.env.LEX_BOT_ID;
        this.botAliasId = process.env.LEX_BOT_ALIAS_ID;
        this.localeId = "en_US";
    }

    async processMessage(userId, message) {
        try {
            const params = {
                botId: this.botId,
                botAliasId: this.botAliasId,
                localeId: this.localeId,
                sessionId: userId,
                text: message,
            };

            const command = new RecognizeTextCommand(params);
            const response = await this.lexClient.send(command);

            return this.formatLexResponse(response);
        } catch (error) {
            console.error('Error processing message with Lex:', error);
            throw error;
        }
    }

    formatLexResponse(lexResponse) {
        if (lexResponse.messages && lexResponse.messages.length > 0) {
            return {
                type: 'BOT_RESPONSE',
                content: lexResponse.messages.map(msg => msg.content).join('\n'),
                intent: lexResponse.interpretations[0]?.intent?.name || 'Unknown',
                confidence: lexResponse.interpretations[0]?.nluConfidence || 0
            };
        }
        
        return {
            type: 'BOT_RESPONSE',
            content: "I'm sorry, I couldn't understand that.",
            intent: 'Unknown',
            confidence: 0
        };
    }
}

// Intent Handlers
const intentHandlers = {
    Greeting: async (event) => {
        const timeOfDay = getTimeOfDay();
        return {
            messages: [{
                content: `Good ${timeOfDay}! How can I help you today?`,
                contentType: 'PlainText'
            }]
        };
    },

    Help: async (event) => {
        return {
            messages: [{
                content: 'I can help you with:\n' +
                        '- Checking your order status\n' +
                        '- Account information\n' +
                        '- Delivery tracking\n' +
                        'Just let me know what you need!',
                contentType: 'PlainText'
            }]
        };
    },

    Status: async (event) => {
        const statusType = event.sessionState.intent.slots.StatusType;
        let response = '';

        switch (statusType.value?.interpretedValue) {
            case 'order':
                response = 'Your last order #12345 is being processed and will be shipped soon.';
                break;
            case 'account':
                response = 'Your account is active and in good standing.';
                break;
            case 'delivery':
                response = 'Your delivery is scheduled for tomorrow between 2-4 PM.';
                break;
            default:
                response = 'Please specify what type of status you would like to check.';
        }

        return {
            messages: [{
                content: response,
                contentType: 'PlainText'
            }]
        };
    }
};

function getTimeOfDay() {
    const hour = new Date().getHours();
    if (hour < 12) return 'morning';
    if (hour < 18) return 'afternoon';
    return 'evening';
}

// Lambda Handler
exports.handler = async (event) => {
    console.log('Received event:', JSON.stringify(event, null, 2));

    const intentName = event.sessionState.intent.name;
    const intentHandler = intentHandlers[intentName];

    if (!intentHandler) {
        return {
            messages: [{
                content: "I'm sorry, I don't know how to handle that request.",
                contentType: 'PlainText'
            }]
        };
    }

    try {
        return await intentHandler(event);
    } catch (error) {
        console.error(`Error handling intent ${intentName}:`, error);
        return {
            messages: [{
                content: "I'm sorry, something went wrong. Please try again later.",
                contentType: 'PlainText'
            }]
        };
    }
};

3.3 Integración con WebSocket

javascript
// backend/lib/lexIntegration.js
const { WebSocketManager } = require('./websocket');
const { LexBotHandler } = require('./lexbot/handler');
const { ChatRepository } = require('./dynamodb');

class LexChatIntegration {
    constructor() {
        this.lexBot = new LexBotHandler();
        this.chatRepo = new ChatRepository();
        this.wsManager = null; // Se inicializa en el handler
    }

    async handleMessage(event, context) {
        const body = JSON.parse(event.body);
        const connectionId = event.requestContext.connectionId;
        const userId = event.requestContext.authorizer.claims.sub;

        // Inicializar WebSocket Manager
        this.wsManager = new WebSocketManager(
            event.requestContext.domainName,
            event.requestContext.stage
        );

        try {
            // Procesar mensaje con Lex
            const botResponse = await this.lexBot.processMessage(userId, body.message);

            // Guardar mensaje del usuario
            await this.chatRepo.saveMessage({
                connectionId,
                userId,
                type: 'USER_MESSAGE',
                content: body.message,
                timestamp: Date.now()
            });

            // Guardar respuesta del bot
            await this.chatRepo.saveMessage({
                connectionId,
                userId: 'BOT',
                type: 'BOT_RESPONSE',
                content: botResponse.content,
                metadata: {
                    intent: botResponse.intent,
                    confidence: botResponse.confidence
                },
                timestamp: Date.now()
            });

            // Enviar respuesta al usuario
            await this.wsManager.sendMessage(connectionId, {
                type: 'BOT_RESPONSE',
                content: botResponse.content,
                timestamp: Date.now()
            });

            return {
                statusCode: 200,
                body: 'Message processed successfully'
            };
        } catch (error) {
            console.error('Error processing message:', error);
            return {
                statusCode: 500,
                body: 'Error processing message'
            };
        }
    }
}

module.exports = LexChatIntegration;

Verificación de la Etapa 3

Checklist:

  • [ ] Bot de Lex configurado
  • [ ] Intents implementados
  • [ ] Integración funcionando
  • [ ] Respuestas procesadas
  • [ ] Errores manejados
  • [ ] Logging configurado
  • [ ] Mensajes persistidos

Pruebas Recomendadas:

  1. Verificar intents del bot
  2. Probar respuestas automáticas
  3. Validar integración WebSocket
  4. Comprobar persistencia

Troubleshooting Común:

  1. Problemas de Lex:

    • Verificar configuración
    • Revisar intents
    • Comprobar permisos
  2. Errores de Integración:

    • Verificar conexión
    • Revisar manejo de mensajes
    • Comprobar respuestas
  3. Problemas de Persistencia:

    • Verificar guardado
    • Revisar formato
    • Comprobar consultas
  4. Errores de WebSocket:

    • Verificar conexión
    • Revisar envío
    • Comprobar recepción

Etapa 4: Frontend Base

Objetivos:

  1. Implementar interfaz de usuario
  2. Configurar cliente WebSocket
  3. Integrar autenticación
  4. Crear componentes del chat

4.1 Componente Principal del Chat

tsx
import React, { useEffect, useState } from 'react';
import { useWebSocket } from '../../hooks/useWebSocket';
import { useAuth } from '../../hooks/useAuth';
import MessageList from '../MessageList';
import UserInput from '../UserInput';

export default function ChatComponent() {
  const { user } = useAuth();
  const [messages, setMessages] = useState([]);
  const { 
    sendMessage, 
    lastMessage, 
    readyState 
  } = useWebSocket(`${process.env.REACT_APP_WS_URL}?token=${user.token}`);

  useEffect(() => {
    if (lastMessage) {
      const msgData = JSON.parse(lastMessage.data);
      setMessages(prev => [...prev, msgData]);
    }
  }, [lastMessage]);

  const handleSendMessage = (text) => {
    if (text.trim() && readyState === 1) {
      sendMessage(JSON.stringify({
        action: 'sendmessage',
        message: text
      }));
    }
  };

  return (
    <div className="flex flex-col h-screen bg-gray-100">
      <div className="flex-none bg-white shadow">
        <header className="px-4 py-3">
          <h1 className="text-xl font-semibold">Chat Assistant</h1>
          <div className="text-sm text-gray-500">
            {readyState === 1 ? 'Connected' : 'Connecting...'}
          </div>
        </header>
      </div>
      
      <div className="flex-grow overflow-hidden">
        <MessageList 
          messages={messages} 
          currentUser={user}
        />
      </div>
      
      <div className="flex-none p-4 bg-white border-t">
        <UserInput 
          onSendMessage={handleSendMessage}
          disabled={readyState !== 1}
        />
      </div>
    </div>
  );
}

4.2 Hooks Personalizados

javascript
// frontend/src/hooks/useWebSocket.js
import { useEffect, useCallback, useState } from 'react';
import useWebSocket from 'react-use-websocket';

export const useChat = (url) => {
  const [messageHistory, setMessageHistory] = useState([]);
  const [connectionStatus, setConnectionStatus] = useState('disconnected');

  const {
    sendMessage,
    lastMessage,
    readyState,
    getWebSocket
  } = useWebSocket(url, {
    onOpen: () => setConnectionStatus('connected'),
    onClose: () => setConnectionStatus('disconnected'),
    onError: () => setConnectionStatus('error'),
    shouldReconnect: (closeEvent) => true,
    reconnectAttempts: 10,
    reconnectInterval: 3000
  });

  useEffect(() => {
    if (lastMessage) {
      try {
        const data = JSON.parse(lastMessage.data);
        setMessageHistory(prev => [...prev, data]);
      } catch (e) {
        console.error('Error parsing message:', e);
      }
    }
  }, [lastMessage]);

  const send = useCallback((message) => {
    if (readyState === 1) {
      sendMessage(JSON.stringify(message));
      return true;
    }
    return false;
  }, [readyState, sendMessage]);

  const clearHistory = useCallback(() => {
    setMessageHistory([]);
  }, []);

  return {
    send,
    messages: messageHistory,
    status: connectionStatus,
    clearHistory,
    readyState
  };
};

// frontend/src/hooks/useAuth.js
import { useState, useEffect, createContext, useContext } from 'react';
import { Auth } from 'aws-amplify';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkUser();
  }, []);

  async function checkUser() {
    try {
      const userData = await Auth.currentAuthenticatedUser();
      setUser(userData);
    } catch (error) {
      setUser(null);
    } finally {
      setLoading(false);
    }
  }

  async function signIn(username, password) {
    try {
      const user = await Auth.signIn(username, password);
      setUser(user);
      return user;
    } catch (error) {
      throw error;
    }
  }

  async function signOut() {
    try {
      await Auth.signOut();
      setUser(null);
    } catch (error) {
      throw error;
    }
  }

  const value = {
    user,
    loading,
    signIn,
    signOut
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

4.3 Componentes de Mensajes

tsx
// MessageList Component
export const MessageList = ({ messages, currentUser }) => {
  const messagesEndRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  return (
    <div className="flex flex-col space-y-4 p-4 overflow-y-auto">
      {messages.map((msg, index) => (
        <MessageBubble
          key={msg.id || index}
          message={msg}
          isOwn={msg.userId === currentUser?.sub}
        />
      ))}
      <div ref={messagesEndRef} />
    </div>
  );
};

// MessageBubble Component
export const MessageBubble = ({ message, isOwn }) => {
  const bubbleClass = isOwn
    ? "bg-blue-500 text-white self-end"
    : message.type === 'BOT_RESPONSE'
    ? "bg-gray-200 text-gray-800"
    : "bg-white text-gray-800";

  return (
    <div className={`max-w-[70%] rounded-lg px-4 py-2 shadow ${bubbleClass}`}>
      {message.type === 'BOT_RESPONSE' && (
        <div className="text-xs text-gray-500 mb-1">Bot</div>
      )}
      <div>{message.content}</div>
      <div className="text-xs opacity-70 mt-1">
        {new Date(message.timestamp).toLocaleTimeString()}
      </div>
    </div>
  );
};

// UserInput Component
export const UserInput = ({ onSendMessage, disabled }) => {
  const [message, setMessage] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (message.trim() && !disabled) {
      onSendMessage(message);
      setMessage('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="flex items-center space-x-2">
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="Type a message..."
        disabled={disabled}
        className="flex-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
      />
      <button
        type="submit"
        disabled={disabled || !message.trim()}
        className={`px-4 py-2 rounded-lg ${
          disabled
            ? 'bg-gray-300 cursor-not-allowed'
            : 'bg-blue-500 hover:bg-blue-600'
        } text-white`}
      >
        Send
      </button>
    </form>
  );
};

4.4 Configuración de Amplify

javascript
// src/config/amplify.js
import { Amplify } from 'aws-amplify';

Amplify.configure({
  Auth: {
    region: process.env.REACT_APP_REGION,
    userPoolId: process.env.REACT_APP_USER_POOL_ID,
    userPoolWebClientId: process.env.REACT_APP_USER_POOL_CLIENT_ID,
    mandatorySignIn: true,
    oauth: {
      domain: `${process.env.REACT_APP_AUTH_DOMAIN}`,
      scope: ['email', 'openid', 'profile'],
      redirectSignIn: process.env.REACT_APP_REDIRECT_SIGN_IN,
      redirectSignOut: process.env.REACT_APP_REDIRECT_SIGN_OUT,
      responseType: 'code'
    }
  },
  API: {
    endpoints: [
      {
        name: 'api',
        endpoint: process.env.REACT_APP_API_URL,
        region: process.env.REACT_APP_REGION
      }
    ]
  }
});

export default Amplify;

Verificación de la Etapa 4

Checklist:

  • [ ] Interfaz implementada
  • [ ] WebSocket configurado
  • [ ] Autenticación funcionando
  • [ ] Componentes renderizando
  • [ ] Estilos aplicados
  • [ ] Estado manejado
  • [ ] Errores controlados

Pruebas Recomendadas:

  1. Verificar conexión WebSocket
  2. Probar envío/recepción
  3. Validar autenticación
  4. Comprobar UI/UX

Troubleshooting Común:

  1. Problemas de WebSocket:

    • Verificar conexión
    • Revisar token
    • Comprobar reconexión
  2. Errores de Autenticación:

    • Verificar tokens
    • Revisar configuración
    • Comprobar permisos
  3. Problemas de UI:

    • Verificar renderizado
    • Revisar responsive
    • Comprobar estados
  4. Errores de Estado:

    • Verificar actualización
    • Revisar memoria
    • Comprobar ciclo de vida

Etapa 5: Integraciones y Pruebas

Objetivos:

  1. Implementar tests automatizados
  2. Configurar logging y monitoreo
  3. Integrar los componentes
  4. Establecer pruebas end-to-end

5.1 Tests Unitarios

javascript
// tests/unit/websocket.test.js
import { jest } from '@jest/globals';
import { WebSocketManager } from '../../backend/lib/websocket';

describe('WebSocketManager', () => {
  let wsManager;

  beforeEach(() => {
    wsManager = new WebSocketManager('test-domain', 'test-stage');
    wsManager.apiGateway.send = jest.fn();
  });

  test('should broadcast message to all connections', async () => {
    const connections = [
      { connectionId: 'conn1' },
      { connectionId: 'conn2' }
    ];
    const message = { type: 'TEST', content: 'test message' };

    await wsManager.broadcast(connections, message);

    expect(wsManager.apiGateway.send).toHaveBeenCalledTimes(2);
    expect(wsManager.apiGateway.send).toHaveBeenCalledWith(
      expect.objectContaining({
        input: {
          ConnectionId: 'conn1',
          Data: JSON.stringify(message)
        }
      })
    );
  });

  test('should handle stale connections', async () => {
    const connectionId = 'stale-conn';
    const message = { type: 'TEST', content: 'test message' };

    wsManager.apiGateway.send.mockRejectedValueOnce({ statusCode: 410 });

    const result = await wsManager.sendMessage(connectionId, message);
    expect(result).toEqual({ staleConnection: connectionId });
  });
});

// tests/unit/lex.test.js
import { LexBotHandler } from '../../backend/functions/lexbot/handler';

describe('LexBotHandler', () => {
  let lexHandler;

  beforeEach(() => {
    lexHandler = new LexBotHandler();
    lexHandler.lexClient.send = jest.fn();
  });

  test('should process user message', async () => {
    const userId = 'test-user';
    const message = 'hello';
    const mockResponse = {
      messages: [{ content: 'Hi there!' }],
      interpretations: [{
        intent: { name: 'Greeting' },
        nluConfidence: 0.98
      }]
    };

    lexHandler.lexClient.send.mockResolvedValueOnce(mockResponse);

    const result = await lexHandler.processMessage(userId, message);

    expect(result).toEqual({
      type: 'BOT_RESPONSE',
      content: 'Hi there!',
      intent: 'Greeting',
      confidence: 0.98
    });
  });

  test('should handle invalid responses', async () => {
    const userId = 'test-user';
    const message = 'invalid';

    lexHandler.lexClient.send.mockResolvedValueOnce({});

    const result = await lexHandler.processMessage(userId, message);

    expect(result).toEqual({
      type: 'BOT_RESPONSE',
      content: "I'm sorry, I couldn't understand that.",
      intent: 'Unknown',
      confidence: 0
    });
  });
});

5.2 Tests de Integración

javascript
// tests/integration/chat.test.js
import { createServer } from '../../backend/server';
import { WebSocket } from 'ws';
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { AuthService } from '../../backend/lib/auth';

describe('Chat Integration Tests', () => {
  let server;
  let clientA;
  let clientB;
  let dynamoDB;
  let authService;

  beforeAll(async () => {
    server = await createServer();
    dynamoDB = new DynamoDB({ region: 'local', endpoint: 'http://localhost:8000' });
    authService = new AuthService();
  });

  beforeEach(async () => {
    // Create test users and get tokens
    const tokenA = await authService.generateToken('userA');
    const tokenB = await authService.generateToken('userB');

    // Connect websocket clients
    clientA = new WebSocket(`ws://localhost:${server.port}?token=${tokenA}`);
    clientB = new WebSocket(`ws://localhost:${server.port}?token=${tokenB}`);

    // Wait for connections
    await Promise.all([
      new Promise(resolve => clientA.on('open', resolve)),
      new Promise(resolve => clientB.on('open', resolve))
    ]);
  });

  afterEach(() => {
    clientA.close();
    clientB.close();
  });

  afterAll(async () => {
    await server.close();
  });

  test('should handle message broadcast', (done) => {
    const testMessage = {
      action: 'sendmessage',
      message: 'Hello World'
    };

    // Store received messages
    const messagesReceived = [];

    // Listen for messages on client B
    clientB.on('message', (data) => {
      const message = JSON.parse(data);
      messagesReceived.push(message);
      
      // Verify message received
      if (messagesReceived.length === 1) {
        expect(message.content).toBe('Hello World');
        expect(message.type).toBe('USER_MESSAGE');
        done();
      }
    });

    // Send message from client A
    clientA.send(JSON.stringify(testMessage));
  });

  test('should handle bot responses', (done) => {
    const testMessage = {
      action: 'sendmessage',
      message: 'help'
    };

    // Listen for bot response
    clientA.on('message', (data) => {
      const message = JSON.parse(data);
      
      if (message.type === 'BOT_RESPONSE') {
        expect(message.content).toContain('I can help you with');
        done();
      }
    });

    // Send help message
    clientA.send(JSON.stringify(testMessage));
  });

  test('should persist messages in DynamoDB', async () => {
    const testMessage = {
      action: 'sendmessage',
      message: 'Test persistence'
    };

    // Send message
    clientA.send(JSON.stringify(testMessage));

    // Wait for message processing
    await new Promise(resolve => setTimeout(resolve, 1000));

    // Query DynamoDB
    const result = await dynamoDB.scan({
      TableName: process.env.MESSAGES_TABLE,
      FilterExpression: 'content = :content',
      ExpressionAttributeValues: {
        ':content': { S: 'Test persistence' }
      }
    }).promise();

    expect(result.Items.length).toBe(1);
    expect(result.Items[0].type.S).toBe('USER_MESSAGE');
  });
});

5.3 End-to-End Tests

javascript
// tests/e2e/chat.spec.js
import { test, expect } from '@playwright/test';

test.describe('Chat Application', () => {
  test.beforeEach(async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'testpass');
    await page.click('button[type="submit"]');
    
    // Wait for chat to load
    await page.waitForSelector('.chat-container');
  });

  test('should send and receive messages', async ({ page }) => {
    const testMessage = 'Hello from E2E test';

    // Send message
    await page.fill('.message-input', testMessage);
    await page.click('button[type="submit"]');

    // Wait for message to appear
    const message = await page.waitForSelector(
      `.message-bubble:has-text("${testMessage}")`
    );
    expect(message).toBeTruthy();
  });

  test('should handle bot responses', async ({ page }) => {
    // Send help command
    await page.fill('.message-input', 'help');
    await page.click('button[type="submit"]');

    // Wait for bot response
    const botResponse = await page.waitForSelector(
      '.bot-message:has-text("I can help you")'
    );
    expect(botResponse).toBeTruthy();
  });

  test('should handle connection issues', async ({ page, context }) => {
    // Simulate offline mode
    await context.setOffline(true);

    // Try to send message
    await page.fill('.message-input', 'Test offline');
    await page.click('button[type="submit"]');

    // Check for error message
    const error = await page.waitForSelector(
      '.error-message:has-text("Connection lost")'
    );
    expect(error).toBeTruthy();

    // Restore connection
    await context.setOffline(false);

    // Wait for reconnection message
    const reconnected = await page.waitForSelector(
      '.status-message:has-text("Connected")'
    );
    expect(reconnected).toBeTruthy();
  });

  test('should persist chat history', async ({ page }) => {
    // Send multiple messages
    const messages = ['Message 1', 'Message 2', 'Message 3'];
    
    for (const message of messages) {
      await page.fill('.message-input', message);
      await page.click('button[type="submit"]');
      await page.waitForSelector(`.message-bubble:has-text("${message}")`);
    }

    // Reload page
    await page.reload();

    // Verify messages are still present
    for (const message of messages) {
      const messageElement = await page.waitForSelector(
        `.message-bubble:has-text("${message}")`
      );
      expect(messageElement).toBeTruthy();
    }
  });
});

5.4 Configuración de CI/CD para Tests

yaml
# .github/workflows/tests.yml
name: Test Suite

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      dynamodb-local:
        image: amazon/dynamodb-local
        ports:
          - 8000:8000

    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
          cache: 'npm'

      - name: Install Dependencies
        run: npm ci

      - name: Run Linter
        run: npm run lint

      - name: Run Unit Tests
        run: npm run test:unit
        env:
          AWS_REGION: us-east-1
          MESSAGES_TABLE: test-messages
          CONNECTIONS_TABLE: test-connections

      - name: Run Integration Tests
        run: npm run test:integration
        env:
          AWS_REGION: us-east-1
          DYNAMODB_ENDPOINT: http://localhost:8000
          MESSAGES_TABLE: test-messages
          CONNECTIONS_TABLE: test-connections

      - name: Run E2E Tests
        run: |
          npm run build
          npm run start:test &
          npx wait-on http://localhost:3000
          npm run test:e2e
        env:
          CI: true

      - name: Upload Test Coverage
        uses: codecov/codecov-action@v2
        with:
          files: ./coverage/lcov.info

      - name: Publish Test Results
        uses: EnricoMi/publish-unit-test-result-action@v1
        if: always()
        with:
          files: test-results/**/*.xml

Verificación de la Etapa 5

Checklist:

  • [ ] Tests unitarios implementados
  • [ ] Tests de integración funcionando
  • [ ] E2E tests configurados
  • [ ] CI/CD establecido
  • [ ] Cobertura de código adecuada
  • [ ] Linting configurado
  • [ ] Reportes generados

Pruebas Recomendadas:

  1. Ejecutar suite completa
  2. Verificar cobertura
  3. Validar CI/CD
  4. Probar casos edge

Troubleshooting Común:

  1. Problemas de Tests:

    • Verificar mocks
    • Revisar asincronía
    • Comprobar setup
  2. Errores de Integración:

    • Verificar servicios
    • Revisar conexiones
    • Comprobar estados
  3. Problemas de E2E:

    • Verificar navegador
    • Revisar timeouts
    • Comprobar selectores
  4. Errores de CI/CD:

    • Verificar configuración
    • Revisar dependencias
    • Comprobar permisos

Etapa 6: Optimización y Producción

Objetivos:

  1. Implementar optimizaciones de rendimiento
  2. Configurar monitoreo en producción
  3. Establecer sistema de logging
  4. Preparar para producción

6.1 Optimizaciones de Rendimiento

javascript
// backend/lib/cache.js
const Redis = require('ioredis');
const { promisify } = require('util');

class CacheManager {
    constructor() {
        this.redis = new Redis({
            host: process.env.REDIS_HOST,
            port: process.env.REDIS_PORT,
            maxRetriesPerRequest: 3
        });
        
        this.defaultTTL = 3600; // 1 hora
    }

    async set(key, value, ttl = this.defaultTTL) {
        try {
            await this.redis.set(key, JSON.stringify(value), 'EX', ttl);
            return true;
        } catch (error) {
            console.error('Cache set error:', error);
            return false;
        }
    }

    async get(key) {
        try {
            const value = await this.redis.get(key);
            return value ? JSON.parse(value) : null;
        } catch (error) {
            console.error('Cache get error:', error);
            return null;
        }
    }

    async invalidate(pattern) {
        try {
            const keys = await this.redis.keys(pattern);
            if (keys.length > 0) {
                await this.redis.del(...keys);
            }
            return true;
        } catch (error) {
            console.error('Cache invalidation error:', error);
            return false;
        }
    }
}

// backend/lib/rateLimit.js
class RateLimiter {
    constructor(cache) {
        this.cache = cache;
        this.windowMs = 60000; // 1 minuto
        this.max = 100; // máximo de requests por ventana
    }

    async checkLimit(userId) {
        const key = `ratelimit:${userId}`;
        const currentCount = await this.cache.get(key) || 0;

        if (currentCount >= this.max) {
            return false;
        }

        await this.cache.set(key, currentCount + 1, this.windowMs / 1000);
        return true;
    }
}

// backend/lib/connectionPool.js
class ConnectionPool {
    constructor() {
        this.connections = new Map();
        this.maxConnections = 1000;
    }

    add(connectionId, connection) {
        if (this.connections.size >= this.maxConnections) {
            throw new Error('Connection pool limit reached');
        }
        this.connections.set(connectionId, connection);
    }

    remove(connectionId) {
        this.connections.delete(connectionId);
    }

    get(connectionId) {
        return this.connections.get(connectionId);
    }

    broadcast(message, excludeId = null) {
        for (const [id, connection] of this.connections) {
            if (id !== excludeId) {
                try {
                    connection.send(JSON.stringify(message));
                } catch (error) {
                    console.error(`Error broadcasting to ${id}:`, error);
                    this.remove(id);
                }
            }
        }
    }
}

6.2 Monitoreo en Producción

javascript
// backend/lib/monitoring.js
const { CloudWatch } = require('@aws-sdk/client-cloudwatch');
const { performance } = require('perf_hooks');

class Monitoring {
    constructor() {
        this.cloudWatch = new CloudWatch();
        this.namespace = 'ChatApplication';
    }

    async trackMetric(name, value, unit = 'Count', dimensions = {}) {
        try {
            const metric = {
                MetricData: [{
                    MetricName: name,
                    Value: value,
                    Unit: unit,
                    Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({
                        Name,
                        Value
                    })),
                    Timestamp: new Date()
                }],
                Namespace: this.namespace
            };

            await this.cloudWatch.putMetricData(metric);
        } catch (error) {
            console.error('Error tracking metric:', error);
        }
    }

    async trackLatency(operation, startTime) {
        const duration = performance.now() - startTime;
        await this.trackMetric(
            `${operation}Latency`,
            duration,
            'Milliseconds',
            { Operation: operation }
        );
    }

    async trackError(type, error) {
        await this.trackMetric(
            'Errors',
            1,
            'Count',
            { 
                ErrorType: type,
                ErrorMessage: error.message
            }
        );
    }
}

// backend/lib/healthCheck.js
class HealthCheck {
    constructor(services) {
        this.services = services;
    }

    async checkHealth() {
        const health = {
            status: 'ok',
            timestamp: new Date().toISOString(),
            services: {}
        };

        for (const [name, service] of Object.entries(this.services)) {
            try {
                const status = await service.check();
                health.services[name] = status;
            } catch (error) {
                health.services[name] = {
                    status: 'error',
                    error: error.message
                };
                health.status = 'error';
            }
        }

        return health;
    }
}

6.3 Sistema de Logging

javascript
// backend/lib/logger.js
const winston = require('winston');
const CloudWatchTransport = require('winston-cloudwatch');

class Logger {
    constructor(service) {
        this.logger = winston.createLogger({
            level: process.env.LOG_LEVEL || 'info',
            format: winston.format.combine(
                winston.format.timestamp(),
                winston.format.json()
            ),
            defaultMeta: { service }
        });

        // Console transport para desarrollo
        if (process.env.NODE_ENV !== 'production') {
            this.logger.add(new winston.transports.Console({
                format: winston.format.simple()
            }));
        }

        // CloudWatch transport para producción
        if (process.env.NODE_ENV === 'production') {
            this.logger.add(new CloudWatchTransport({
                logGroupName: `/aws/lambda/${service}`,
                logStreamName: `${process.env.AWS_REGION}-${new Date().toISOString().slice(0,10)}`,
                awsRegion: process.env.AWS_REGION
            }));
        }
    }

    log(level, message, meta = {}) {
        this.logger.log(level, message, {
            timestamp: new Date().toISOString(),
            ...meta
        });
    }

    error(message, error, meta = {}) {
        this.logger.error(message, {
            error: {
                message: error.message,
                stack: error.stack,
                name: error.name
            },
            ...meta
        });
    }

    createRequestLogger() {
        return (req, res, next) => {
            const start = process.hrtime();

            res.on('finish', () => {
                const [seconds, nanoseconds] = process.hrtime(start);
                const duration = seconds * 1000 + nanoseconds / 1000000;

                this.log('info', 'Request processed', {
                    method: req.method,
                    path: req.path,
                    statusCode: res.statusCode,
                    duration: `${duration.toFixed(2)}ms`,
                    requestId: req.id,
                    userId: req.user?.id
                });
            });

            next();
        };
    }
}

6.4 Configuración de Producción

yaml
# config/production.yml
service: chat-application

provider:
  name: aws
  runtime: nodejs16.x
  stage: prod
  region: ${opt:region, 'us-east-1'}
  memorySize: 256
  timeout: 30
  environment:
    NODE_ENV: production
    LOG_LEVEL: info
    REDIS_HOST: ${ssm:/prod/redis/host}
    REDIS_PORT: ${ssm:/prod/redis/port}

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:*
      Resource: 
        - !GetAtt MessagesTable.Arn
        - !GetAtt ConnectionsTable.Arn
    - Effect: Allow
      Action:
        - execute-api:*
      Resource: "*"
    - Effect: Allow
      Action:
        - cloudwatch:PutMetricData
      Resource: "*"

functions:
  wsConnect:
    handler: handler.wsConnect
    events:
      - websocket:
          route: $connect
    reservedConcurrency: 50

  wsDisconnect:
    handler: handler.wsDisconnect
    events:
      - websocket:
          route: $disconnect
    reservedConcurrency: 50

  wsMessage:
    handler: handler.wsMessage
    events:
      - websocket:
          route: $default
    reservedConcurrency: 100

resources:
  Resources:
    MessagesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:provider.stage}-messages
        AttributeDefinitions:
          - AttributeName: messageId
            AttributeType: S
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: messageId
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: UserMessages
            KeySchema:
              - AttributeName: userId
                KeyType: HASH
            Projection:
              ProjectionType: ALL

    ConnectionsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        BillingMode: PAY_PER_REQUEST
        TableName: ${self:provider.stage}-connections
        AttributeDefinitions:
          - AttributeName: connectionId
            AttributeType: S
        KeySchema:
          - AttributeName: connectionId
            KeyType: HASH

custom:
  webpack:
    webpackConfig: webpack.prod.js
    includeModules: true
    packager: npm
  prune:
    automatic: true
    number: 3

Verificación Final

Checklist:

  • [ ] Optimizaciones implementadas
  • [ ] Monitoreo configurado
  • [ ] Logging establecido
  • [ ] Configuración de producción lista
  • [ ] Performance optimizado
  • [ ] Sistema escalable
  • [ ] Documentación completa

Pruebas Finales:

  1. Verificar rendimiento
  2. Probar escalabilidad
  3. Validar monitoreo
  4. Comprobar logs

Troubleshooting Común:

  1. Problemas de Performance:

    • Verificar caché
    • Revisar conexiones
    • Comprobar memoria
  2. Errores de Monitoreo:

    • Verificar métricas
    • Revisar alarmas
    • Comprobar dashboards
  3. Problemas de Logging:

    • Verificar niveles
    • Revisar retención
    • Comprobar formato
  4. Errores de Producción:

    • Verificar configuración
    • Revisar permisos
    • Comprobar recursos