✨ feat(admin): implementar painel administrativo base
Estrutura a fase 3 do orquestrador-admin com uma camada web propria para o painel interno, assets dedicados e uma dashboard administrativa em Bootstrap para o fluxo do staff. Integra autenticacao web com cookies httpOnly, protege a navegacao do painel, adiciona snapshots de configuracao do sistema e entrega as primeiras superficies de gestao de tools para revisao, aprovacao e ativacao. Tambem amplia a cobertura com testes para bootstrap do app, autenticacao web do painel, configuracao administrativa, governanca de tools e validacao da sessao administrativa no navegador.feat/self-evolving-tools-foundation
parent
82a12ff464
commit
ed1a36ceb6
@ -0,0 +1,170 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from admin_app.api.dependencies import get_settings, require_panel_admin_permission
|
||||||
|
from admin_app.api.schemas import (
|
||||||
|
AdminToolContractsResponse,
|
||||||
|
AdminToolDraftListResponse,
|
||||||
|
AdminToolManagementActionResponse,
|
||||||
|
AdminToolOverviewResponse,
|
||||||
|
AdminToolPublicationListResponse,
|
||||||
|
AdminToolReviewQueueResponse,
|
||||||
|
)
|
||||||
|
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
|
||||||
|
from admin_app.services import ToolManagementService
|
||||||
|
from shared.contracts import AdminPermission
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/panel/tools", tags=["panel-tools"])
|
||||||
|
|
||||||
|
|
||||||
|
def _build_service(settings: AdminSettings) -> ToolManagementService:
|
||||||
|
return ToolManagementService(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/overview",
|
||||||
|
response_model=AdminToolOverviewResponse,
|
||||||
|
)
|
||||||
|
def panel_tools_overview(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_overview_payload()
|
||||||
|
return AdminToolOverviewResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
mode=payload["mode"],
|
||||||
|
metrics=payload["metrics"],
|
||||||
|
workflow=payload["workflow"],
|
||||||
|
actions=_build_panel_actions(settings),
|
||||||
|
next_steps=payload["next_steps"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/contracts",
|
||||||
|
response_model=AdminToolContractsResponse,
|
||||||
|
)
|
||||||
|
def panel_tool_contracts(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_contracts_payload()
|
||||||
|
return AdminToolContractsResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
publication_source_service=payload["publication_source_service"],
|
||||||
|
publication_target_service=payload["publication_target_service"],
|
||||||
|
lifecycle_statuses=payload["lifecycle_statuses"],
|
||||||
|
parameter_types=payload["parameter_types"],
|
||||||
|
publication_fields=payload["publication_fields"],
|
||||||
|
published_tool_fields=payload["published_tool_fields"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/drafts",
|
||||||
|
response_model=AdminToolDraftListResponse,
|
||||||
|
)
|
||||||
|
def panel_tool_drafts(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_drafts_payload()
|
||||||
|
return AdminToolDraftListResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
storage_status=payload["storage_status"],
|
||||||
|
message=payload["message"],
|
||||||
|
drafts=payload["drafts"],
|
||||||
|
supported_statuses=payload["supported_statuses"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/review-queue",
|
||||||
|
response_model=AdminToolReviewQueueResponse,
|
||||||
|
)
|
||||||
|
def panel_tool_review_queue(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_review_queue_payload()
|
||||||
|
return AdminToolReviewQueueResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
queue_mode=payload["queue_mode"],
|
||||||
|
message=payload["message"],
|
||||||
|
items=payload["items"],
|
||||||
|
supported_statuses=payload["supported_statuses"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/publications",
|
||||||
|
response_model=AdminToolPublicationListResponse,
|
||||||
|
)
|
||||||
|
def panel_tool_publications(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_panel_admin_permission(AdminPermission.PUBLISH_TOOLS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_publications_payload()
|
||||||
|
return AdminToolPublicationListResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
source=payload["source"],
|
||||||
|
target_service=payload["target_service"],
|
||||||
|
publications=payload["publications"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_panel_actions(settings: AdminSettings) -> list[AdminToolManagementActionResponse]:
|
||||||
|
return [
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="overview",
|
||||||
|
label="Overview web de tools",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/overview"),
|
||||||
|
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
|
||||||
|
description="Snapshot do dominio de tools pronto para leitura no painel.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="contracts",
|
||||||
|
label="Contratos web de tools",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/contracts"),
|
||||||
|
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
|
||||||
|
description="Base contratual para a tela de revisao e aprovacao.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="review_queue",
|
||||||
|
label="Fila web de revisao",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/review-queue"),
|
||||||
|
required_permission=AdminPermission.REVIEW_TOOL_GENERATIONS,
|
||||||
|
description="Leitura da fila de revisao sob a sessao web do painel.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="publications",
|
||||||
|
label="Publicacoes web",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/publications"),
|
||||||
|
required_permission=AdminPermission.PUBLISH_TOOLS,
|
||||||
|
description="Catalogo de tools ativas e prontas para ativacao no produto.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prefixed_path(api_prefix: str, path: str) -> str:
|
||||||
|
normalized_prefix = api_prefix.rstrip("/")
|
||||||
|
normalized_path = path if path.startswith("/") else f"/{path}"
|
||||||
|
if not normalized_prefix:
|
||||||
|
return normalized_path
|
||||||
|
if normalized_path == "/":
|
||||||
|
return f"{normalized_prefix}/"
|
||||||
|
return f"{normalized_prefix}{normalized_path}"
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from admin_app.api.dependencies import get_settings, require_admin_permission
|
||||||
|
from admin_app.api.schemas import (
|
||||||
|
AdminToolContractsResponse,
|
||||||
|
AdminToolDraftListResponse,
|
||||||
|
AdminToolManagementActionResponse,
|
||||||
|
AdminToolOverviewResponse,
|
||||||
|
AdminToolPublicationListResponse,
|
||||||
|
AdminToolReviewQueueResponse,
|
||||||
|
)
|
||||||
|
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
|
||||||
|
from admin_app.services import ToolManagementService
|
||||||
|
from shared.contracts import AdminPermission
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/tools", tags=["tools"])
|
||||||
|
|
||||||
|
|
||||||
|
def _build_service(settings: AdminSettings) -> ToolManagementService:
|
||||||
|
return ToolManagementService(settings)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/overview",
|
||||||
|
response_model=AdminToolOverviewResponse,
|
||||||
|
)
|
||||||
|
def tools_overview(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_overview_payload()
|
||||||
|
return AdminToolOverviewResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
mode=payload["mode"],
|
||||||
|
metrics=payload["metrics"],
|
||||||
|
workflow=payload["workflow"],
|
||||||
|
actions=_build_actions(settings),
|
||||||
|
next_steps=payload["next_steps"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/contracts",
|
||||||
|
response_model=AdminToolContractsResponse,
|
||||||
|
)
|
||||||
|
def tool_contracts(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_contracts_payload()
|
||||||
|
return AdminToolContractsResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
publication_source_service=payload["publication_source_service"],
|
||||||
|
publication_target_service=payload["publication_target_service"],
|
||||||
|
lifecycle_statuses=payload["lifecycle_statuses"],
|
||||||
|
parameter_types=payload["parameter_types"],
|
||||||
|
publication_fields=payload["publication_fields"],
|
||||||
|
published_tool_fields=payload["published_tool_fields"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/drafts",
|
||||||
|
response_model=AdminToolDraftListResponse,
|
||||||
|
)
|
||||||
|
def tool_drafts(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_drafts_payload()
|
||||||
|
return AdminToolDraftListResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
storage_status=payload["storage_status"],
|
||||||
|
message=payload["message"],
|
||||||
|
drafts=payload["drafts"],
|
||||||
|
supported_statuses=payload["supported_statuses"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/review-queue",
|
||||||
|
response_model=AdminToolReviewQueueResponse,
|
||||||
|
)
|
||||||
|
def tool_review_queue(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_review_queue_payload()
|
||||||
|
return AdminToolReviewQueueResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
queue_mode=payload["queue_mode"],
|
||||||
|
message=payload["message"],
|
||||||
|
items=payload["items"],
|
||||||
|
supported_statuses=payload["supported_statuses"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/publications",
|
||||||
|
response_model=AdminToolPublicationListResponse,
|
||||||
|
)
|
||||||
|
def tool_publications(
|
||||||
|
settings: AdminSettings = Depends(get_settings),
|
||||||
|
_: AuthenticatedStaffPrincipal = Depends(
|
||||||
|
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
service = _build_service(settings)
|
||||||
|
payload = service.build_publications_payload()
|
||||||
|
return AdminToolPublicationListResponse(
|
||||||
|
service="orquestrador-admin",
|
||||||
|
source=payload["source"],
|
||||||
|
target_service=payload["target_service"],
|
||||||
|
publications=payload["publications"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_actions(settings: AdminSettings) -> list[AdminToolManagementActionResponse]:
|
||||||
|
return [
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="overview",
|
||||||
|
label="Overview de tools",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/overview"),
|
||||||
|
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
|
||||||
|
description="Snapshot inicial da governanca de tools no admin.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="contracts",
|
||||||
|
label="Contratos compartilhados",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/contracts"),
|
||||||
|
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
|
||||||
|
description="Enumera lifecycle, tipos de parametro e campos de publicacao.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="drafts",
|
||||||
|
label="Fila de drafts",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/drafts"),
|
||||||
|
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
|
||||||
|
description="Base do cadastro de novas tools e estados vazios da fase atual.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="review_queue",
|
||||||
|
label="Fila de revisao",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/review-queue"),
|
||||||
|
required_permission=AdminPermission.REVIEW_TOOL_GENERATIONS,
|
||||||
|
description="Superficie para validacao, revisao tecnica e aprovacao humana.",
|
||||||
|
),
|
||||||
|
AdminToolManagementActionResponse(
|
||||||
|
key="publications",
|
||||||
|
label="Catalogo de publicacoes",
|
||||||
|
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/publications"),
|
||||||
|
required_permission=AdminPermission.PUBLISH_TOOLS,
|
||||||
|
description="Catalogo bootstrap de tools ativas voltadas ao runtime de produto.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prefixed_path(api_prefix: str, path: str) -> str:
|
||||||
|
normalized_prefix = api_prefix.rstrip("/")
|
||||||
|
normalized_path = path if path.startswith("/") else f"/{path}"
|
||||||
|
if not normalized_prefix:
|
||||||
|
return normalized_path
|
||||||
|
if normalized_path == "/":
|
||||||
|
return f"{normalized_prefix}/"
|
||||||
|
return f"{normalized_prefix}{normalized_path}"
|
||||||
@ -0,0 +1,305 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from admin_app.core.settings import AdminSettings
|
||||||
|
from shared.contracts import ServiceName, ToolLifecycleStatus, ToolParameterType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BootstrapToolCatalogEntry:
|
||||||
|
tool_name: str
|
||||||
|
display_name: str
|
||||||
|
description: str
|
||||||
|
domain: str
|
||||||
|
parameter_count: int
|
||||||
|
|
||||||
|
|
||||||
|
_BOOTSTRAP_TOOL_CATALOG: tuple[BootstrapToolCatalogEntry, ...] = (
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="consultar_estoque",
|
||||||
|
display_name="Consultar estoque",
|
||||||
|
description="Consulta veiculos disponiveis no estoque comercial.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=4,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="validar_cliente_venda",
|
||||||
|
display_name="Validar cliente para venda",
|
||||||
|
description="Avalia elegibilidade de credito para operacoes de venda.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=2,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="avaliar_veiculo_troca",
|
||||||
|
display_name="Avaliar veiculo de troca",
|
||||||
|
description="Estima o valor de entrada de um veiculo usado.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=3,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="agendar_revisao",
|
||||||
|
display_name="Agendar revisao",
|
||||||
|
description="Abre um agendamento de revisao ou manutencao.",
|
||||||
|
domain="revisao",
|
||||||
|
parameter_count=6,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="listar_agendamentos_revisao",
|
||||||
|
display_name="Listar agendamentos de revisao",
|
||||||
|
description="Consulta a fila de agendamentos de revisao do cliente.",
|
||||||
|
domain="revisao",
|
||||||
|
parameter_count=3,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="cancelar_agendamento_revisao",
|
||||||
|
display_name="Cancelar agendamento de revisao",
|
||||||
|
description="Cancela um agendamento existente por protocolo.",
|
||||||
|
domain="revisao",
|
||||||
|
parameter_count=2,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="editar_data_revisao",
|
||||||
|
display_name="Editar data de revisao",
|
||||||
|
description="Remarca uma revisao para um novo horario.",
|
||||||
|
domain="revisao",
|
||||||
|
parameter_count=2,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="realizar_pedido",
|
||||||
|
display_name="Realizar pedido",
|
||||||
|
description="Efetiva um pedido de compra com o veiculo escolhido.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=2,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="listar_pedidos",
|
||||||
|
display_name="Listar pedidos",
|
||||||
|
description="Consulta pedidos ja abertos pelo cliente.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=3,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="cancelar_pedido",
|
||||||
|
display_name="Cancelar pedido",
|
||||||
|
description="Cancela um pedido existente com motivo registrado.",
|
||||||
|
domain="vendas",
|
||||||
|
parameter_count=2,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="consultar_frota_aluguel",
|
||||||
|
display_name="Consultar frota de aluguel",
|
||||||
|
description="Lista veiculos disponiveis para locacao.",
|
||||||
|
domain="locacao",
|
||||||
|
parameter_count=6,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="abrir_locacao_aluguel",
|
||||||
|
display_name="Abrir locacao de aluguel",
|
||||||
|
description="Inicia um contrato de locacao de veiculo.",
|
||||||
|
domain="locacao",
|
||||||
|
parameter_count=7,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="registrar_devolucao_aluguel",
|
||||||
|
display_name="Registrar devolucao de aluguel",
|
||||||
|
description="Fecha uma locacao e devolve o veiculo para a frota.",
|
||||||
|
domain="locacao",
|
||||||
|
parameter_count=4,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="registrar_pagamento_aluguel",
|
||||||
|
display_name="Registrar pagamento de aluguel",
|
||||||
|
description="Registra comprovantes e pagamentos de contratos de locacao.",
|
||||||
|
domain="locacao",
|
||||||
|
parameter_count=7,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="limpar_contexto_conversa",
|
||||||
|
display_name="Limpar contexto de conversa",
|
||||||
|
description="Reinicia o contexto operacional atual do atendimento.",
|
||||||
|
domain="orquestracao",
|
||||||
|
parameter_count=1,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="continuar_proximo_pedido",
|
||||||
|
display_name="Continuar proximo pedido",
|
||||||
|
description="Retoma o proximo pedido pendente do fluxo atual.",
|
||||||
|
domain="orquestracao",
|
||||||
|
parameter_count=0,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="descartar_pedidos_pendentes",
|
||||||
|
display_name="Descartar pedidos pendentes",
|
||||||
|
description="Descarta apenas a fila pendente de pedidos do contexto.",
|
||||||
|
domain="orquestracao",
|
||||||
|
parameter_count=1,
|
||||||
|
),
|
||||||
|
BootstrapToolCatalogEntry(
|
||||||
|
tool_name="cancelar_fluxo_atual",
|
||||||
|
display_name="Cancelar fluxo atual",
|
||||||
|
description="Interrompe o fluxo corrente sem apagar todo o contexto.",
|
||||||
|
domain="orquestracao",
|
||||||
|
parameter_count=1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
_LIFECYCLE_DESCRIPTIONS = {
|
||||||
|
ToolLifecycleStatus.DRAFT: "Estado inicial de uma tool ainda em definicao.",
|
||||||
|
ToolLifecycleStatus.GENERATED: "Implementacao gerada e pronta para analise tecnica.",
|
||||||
|
ToolLifecycleStatus.VALIDATED: "Tool validada automaticamente com verificacoes basicas.",
|
||||||
|
ToolLifecycleStatus.APPROVED: "Versao revisada e aprovada para publicacao controlada.",
|
||||||
|
ToolLifecycleStatus.ACTIVE: "Tool publicada e apta a abastecer o runtime de produto.",
|
||||||
|
ToolLifecycleStatus.FAILED: "Falha registrada na geracao, validacao ou ativacao.",
|
||||||
|
ToolLifecycleStatus.ARCHIVED: "Versao retirada de circulacao e mantida apenas para historico.",
|
||||||
|
}
|
||||||
|
|
||||||
|
_PARAMETER_TYPE_DESCRIPTIONS = {
|
||||||
|
ToolParameterType.STRING: "Texto livre, codigos e identificadores.",
|
||||||
|
ToolParameterType.INTEGER: "Valores inteiros para limites, anos e contagens.",
|
||||||
|
ToolParameterType.NUMBER: "Valores numericos decimais, como preco e diaria.",
|
||||||
|
ToolParameterType.BOOLEAN: "Marcadores verdadeiro ou falso para decisoes operacionais.",
|
||||||
|
ToolParameterType.OBJECT: "Estruturas compostas para payloads complexos.",
|
||||||
|
ToolParameterType.ARRAY: "Colecoes ordenadas de valores.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ToolManagementService:
|
||||||
|
def __init__(self, settings: AdminSettings):
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def build_overview_payload(self) -> dict:
|
||||||
|
catalog = self.list_publication_catalog()
|
||||||
|
return {
|
||||||
|
"mode": "bootstrap_catalog",
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"key": "active_catalog",
|
||||||
|
"label": "Tools mapeadas",
|
||||||
|
"value": str(len(catalog)),
|
||||||
|
"description": "Catalogo bootstrap refletindo a base de tools conhecida no monorepo.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "lifecycle_stages",
|
||||||
|
"label": "Etapas de lifecycle",
|
||||||
|
"value": str(len(ToolLifecycleStatus)),
|
||||||
|
"description": "Estados compartilhados entre governanca administrativa e publicacao.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parameter_types",
|
||||||
|
"label": "Tipos de parametro",
|
||||||
|
"value": str(len(ToolParameterType)),
|
||||||
|
"description": "Tipos aceitos pelo contrato inicial de publicacao de tools.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "draft_persistence",
|
||||||
|
"label": "Persistencia de drafts",
|
||||||
|
"value": "pendente",
|
||||||
|
"description": "A fase atual entrega as superficies e o contrato; entidades de draft ainda nao existem.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"workflow": self.build_lifecycle_payload(),
|
||||||
|
"next_steps": [
|
||||||
|
"Criar entidades administrativas para ToolDraft, ToolValidationRun e ToolPublication.",
|
||||||
|
"Ligar o formulario de cadastro de novas tools a uma persistencia propria do admin.",
|
||||||
|
"Abrir filas de revisao, aprovacao e ativacao com auditoria ponta a ponta.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_contracts_payload(self) -> dict:
|
||||||
|
return {
|
||||||
|
"publication_source_service": ServiceName.ADMIN,
|
||||||
|
"publication_target_service": ServiceName.PRODUCT,
|
||||||
|
"lifecycle_statuses": self.build_lifecycle_payload(),
|
||||||
|
"parameter_types": [
|
||||||
|
{
|
||||||
|
"code": parameter_type,
|
||||||
|
"label": parameter_type.value.upper(),
|
||||||
|
"description": _PARAMETER_TYPE_DESCRIPTIONS[parameter_type],
|
||||||
|
}
|
||||||
|
for parameter_type in ToolParameterType
|
||||||
|
],
|
||||||
|
"publication_fields": [
|
||||||
|
"source_service",
|
||||||
|
"target_service",
|
||||||
|
"publication_id",
|
||||||
|
"published_tool",
|
||||||
|
"emitted_at",
|
||||||
|
],
|
||||||
|
"published_tool_fields": [
|
||||||
|
"tool_name",
|
||||||
|
"display_name",
|
||||||
|
"description",
|
||||||
|
"version",
|
||||||
|
"status",
|
||||||
|
"parameters",
|
||||||
|
"implementation_module",
|
||||||
|
"implementation_callable",
|
||||||
|
"checksum",
|
||||||
|
"published_at",
|
||||||
|
"published_by",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_drafts_payload(self) -> dict:
|
||||||
|
return {
|
||||||
|
"storage_status": "pending_persistence",
|
||||||
|
"message": (
|
||||||
|
"As rotas de gestao de tools ja existem, mas a persistencia de ToolDraft ainda sera criada nas proximas etapas."
|
||||||
|
),
|
||||||
|
"drafts": [],
|
||||||
|
"supported_statuses": [ToolLifecycleStatus.DRAFT],
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_review_queue_payload(self) -> dict:
|
||||||
|
return {
|
||||||
|
"queue_mode": "bootstrap_empty_state",
|
||||||
|
"message": (
|
||||||
|
"A fila de revisao ainda opera em estado vazio ate a criacao das entidades de geracao e validacao."
|
||||||
|
),
|
||||||
|
"items": [],
|
||||||
|
"supported_statuses": [
|
||||||
|
ToolLifecycleStatus.GENERATED,
|
||||||
|
ToolLifecycleStatus.VALIDATED,
|
||||||
|
ToolLifecycleStatus.APPROVED,
|
||||||
|
ToolLifecycleStatus.FAILED,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_publications_payload(self) -> dict:
|
||||||
|
return {
|
||||||
|
"source": "bootstrap_catalog",
|
||||||
|
"target_service": ServiceName.PRODUCT,
|
||||||
|
"publications": self.list_publication_catalog(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_lifecycle_payload(self) -> list[dict]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"code": status,
|
||||||
|
"label": status.value.replace("_", " ").title(),
|
||||||
|
"description": _LIFECYCLE_DESCRIPTIONS[status],
|
||||||
|
}
|
||||||
|
for status in ToolLifecycleStatus
|
||||||
|
]
|
||||||
|
|
||||||
|
def list_publication_catalog(self) -> list[dict]:
|
||||||
|
published_at = datetime.now(UTC)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"publication_id": f"bootstrap::{entry.tool_name}::v1",
|
||||||
|
"tool_name": entry.tool_name,
|
||||||
|
"display_name": entry.display_name,
|
||||||
|
"description": entry.description,
|
||||||
|
"domain": entry.domain,
|
||||||
|
"version": 1,
|
||||||
|
"status": ToolLifecycleStatus.ACTIVE,
|
||||||
|
"parameter_count": entry.parameter_count,
|
||||||
|
"implementation_module": "app.services.tools.handlers",
|
||||||
|
"implementation_callable": entry.tool_name,
|
||||||
|
"published_by": "bootstrap_catalog",
|
||||||
|
"published_at": published_at,
|
||||||
|
}
|
||||||
|
for entry in _BOOTSTRAP_TOOL_CATALOG
|
||||||
|
]
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelNavigationItem(BaseModel):
|
||||||
|
label: str
|
||||||
|
href: str
|
||||||
|
description: str
|
||||||
|
badge: str | None = None
|
||||||
|
is_active: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelQuickAction(BaseModel):
|
||||||
|
label: str
|
||||||
|
href: str
|
||||||
|
button_class: str = "btn-outline-dark"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelMetric(BaseModel):
|
||||||
|
label: str
|
||||||
|
value: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelModuleCard(BaseModel):
|
||||||
|
eyebrow: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
status_label: str
|
||||||
|
status_variant: str = "secondary"
|
||||||
|
highlights: tuple[str, ...] = ()
|
||||||
|
cta_label: str | None = None
|
||||||
|
href: str | None = None
|
||||||
|
is_available: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelSurfaceLink(BaseModel):
|
||||||
|
method: str
|
||||||
|
label: str
|
||||||
|
href: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelRoadmapItem(BaseModel):
|
||||||
|
step: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
status_label: str
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelHomeView(BaseModel):
|
||||||
|
service: str
|
||||||
|
app_name: str
|
||||||
|
panel_title: str
|
||||||
|
panel_subtitle: str
|
||||||
|
environment: str
|
||||||
|
version: str
|
||||||
|
api_prefix: str
|
||||||
|
release_label: str
|
||||||
|
navigation: tuple[AdminPanelNavigationItem, ...]
|
||||||
|
quick_actions: tuple[AdminPanelQuickAction, ...]
|
||||||
|
metrics: tuple[AdminPanelMetric, ...]
|
||||||
|
modules: tuple[AdminPanelModuleCard, ...]
|
||||||
|
surface_links: tuple[AdminPanelSurfaceLink, ...]
|
||||||
|
roadmap: tuple[AdminPanelRoadmapItem, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class AdminLoginPageView(BaseModel):
|
||||||
|
app_name: str
|
||||||
|
title: str
|
||||||
|
subtitle: str
|
||||||
|
environment: str
|
||||||
|
version: str
|
||||||
|
dashboard_href: str
|
||||||
|
auth_endpoint: str
|
||||||
|
session_endpoint: str
|
||||||
|
logout_endpoint: str
|
||||||
|
email_placeholder: str
|
||||||
|
password_placeholder: str
|
||||||
|
access_token_ttl_label: str
|
||||||
|
refresh_token_ttl_label: str
|
||||||
|
password_policy_label: str
|
||||||
|
security_highlights: tuple[str, ...]
|
||||||
|
integration_notes: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class AdminToolReviewWorkflowStep(BaseModel):
|
||||||
|
eyebrow: str
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
status_label: str
|
||||||
|
status_variant: str = "secondary"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminToolReviewPageView(BaseModel):
|
||||||
|
app_name: str
|
||||||
|
title: str
|
||||||
|
subtitle: str
|
||||||
|
environment: str
|
||||||
|
version: str
|
||||||
|
dashboard_href: str
|
||||||
|
login_href: str
|
||||||
|
overview_endpoint: str
|
||||||
|
contracts_endpoint: str
|
||||||
|
review_queue_endpoint: str
|
||||||
|
publications_endpoint: str
|
||||||
|
workflow: tuple[AdminToolReviewWorkflowStep, ...]
|
||||||
|
review_notes: tuple[str, ...]
|
||||||
|
approval_notes: tuple[str, ...]
|
||||||
|
activation_notes: tuple[str, ...]
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from admin_app.api.dependencies import get_current_panel_staff_principal
|
||||||
|
from admin_app.app_factory import create_app
|
||||||
|
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
|
||||||
|
from shared.contracts import StaffRole
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPanelToolsWebTests(unittest.TestCase):
|
||||||
|
def _build_client_with_role(
|
||||||
|
self,
|
||||||
|
role: StaffRole,
|
||||||
|
settings: AdminSettings | None = None,
|
||||||
|
) -> tuple[TestClient, object]:
|
||||||
|
app = create_app(
|
||||||
|
settings
|
||||||
|
or AdminSettings(
|
||||||
|
admin_auth_token_secret="test-secret",
|
||||||
|
admin_api_prefix="/admin",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal(
|
||||||
|
id=21,
|
||||||
|
email="staff@empresa.com",
|
||||||
|
display_name="Equipe Web",
|
||||||
|
role=role,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
return TestClient(app), app
|
||||||
|
|
||||||
|
def test_panel_tools_overview_is_available_for_staff_session(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/panel/tools/overview")
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["mode"], "bootstrap_catalog")
|
||||||
|
self.assertIn("/admin/panel/tools/contracts", [item["href"] for item in payload["actions"]])
|
||||||
|
|
||||||
|
def test_panel_tools_review_queue_is_available_for_staff_session(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/panel/tools/review-queue")
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()["queue_mode"], "bootstrap_empty_state")
|
||||||
|
|
||||||
|
def test_panel_tools_publications_require_admin_publication_permission(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/panel/tools/publications")
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json()["detail"],
|
||||||
|
"Permissao administrativa insuficiente: 'publish_tools'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_panel_tools_publications_return_catalog_for_admin_session(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.ADMIN)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/panel/tools/publications")
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["target_service"], "product")
|
||||||
|
self.assertGreaterEqual(len(payload["publications"]), 10)
|
||||||
|
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from admin_app.api.dependencies import get_current_staff_principal
|
||||||
|
from admin_app.app_factory import create_app
|
||||||
|
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
|
||||||
|
from shared.contracts import StaffRole
|
||||||
|
|
||||||
|
|
||||||
|
class AdminToolsWebTests(unittest.TestCase):
|
||||||
|
def _build_client_with_role(
|
||||||
|
self,
|
||||||
|
role: StaffRole,
|
||||||
|
settings: AdminSettings | None = None,
|
||||||
|
) -> tuple[TestClient, object]:
|
||||||
|
app = create_app(
|
||||||
|
settings
|
||||||
|
or AdminSettings(
|
||||||
|
admin_auth_token_secret="test-secret",
|
||||||
|
admin_api_prefix="/admin",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app.dependency_overrides[get_current_staff_principal] = lambda: AuthenticatedStaffPrincipal(
|
||||||
|
id=11,
|
||||||
|
email="staff@empresa.com",
|
||||||
|
display_name="Equipe de Tools",
|
||||||
|
role=role,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
return TestClient(app), app
|
||||||
|
|
||||||
|
def test_tools_overview_requires_manage_tool_drafts_permission(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.VIEWER)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/overview", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json()["detail"],
|
||||||
|
"Permissao administrativa insuficiente: 'manage_tool_drafts'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tools_overview_returns_metrics_workflow_and_actions(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/overview", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["service"], "orquestrador-admin")
|
||||||
|
self.assertEqual(payload["mode"], "bootstrap_catalog")
|
||||||
|
self.assertEqual(payload["metrics"][0]["value"], "18")
|
||||||
|
self.assertIn("active", [item["code"] for item in payload["workflow"]])
|
||||||
|
self.assertIn("/admin/tools/contracts", [item["href"] for item in payload["actions"]])
|
||||||
|
self.assertIn("ToolDraft", payload["next_steps"][0])
|
||||||
|
|
||||||
|
def test_tools_contracts_return_shared_contract_snapshot(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/contracts", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["publication_source_service"], "admin")
|
||||||
|
self.assertEqual(payload["publication_target_service"], "product")
|
||||||
|
self.assertIn("draft", [item["code"] for item in payload["lifecycle_statuses"]])
|
||||||
|
self.assertIn("string", [item["code"] for item in payload["parameter_types"]])
|
||||||
|
self.assertIn("published_tool", payload["publication_fields"])
|
||||||
|
|
||||||
|
def test_tools_drafts_return_empty_state_until_persistence_exists(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/drafts", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["storage_status"], "pending_persistence")
|
||||||
|
self.assertEqual(payload["drafts"], [])
|
||||||
|
self.assertEqual(payload["supported_statuses"], ["draft"])
|
||||||
|
|
||||||
|
def test_tools_review_queue_is_available_for_staff(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["queue_mode"], "bootstrap_empty_state")
|
||||||
|
self.assertEqual(payload["items"], [])
|
||||||
|
self.assertIn("validated", payload["supported_statuses"])
|
||||||
|
|
||||||
|
def test_tools_publications_require_publish_tools_permission(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.STAFF)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json()["detail"],
|
||||||
|
"Permissao administrativa insuficiente: 'publish_tools'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_tools_publications_return_bootstrap_catalog_for_admin(self):
|
||||||
|
client, app = self._build_client_with_role(StaffRole.ADMIN)
|
||||||
|
try:
|
||||||
|
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["source"], "bootstrap_catalog")
|
||||||
|
self.assertEqual(payload["target_service"], "product")
|
||||||
|
self.assertGreaterEqual(len(payload["publications"]), 10)
|
||||||
|
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
|
||||||
|
first = payload["publications"][0]
|
||||||
|
self.assertEqual(first["status"], "active")
|
||||||
|
self.assertEqual(first["implementation_module"], "app.services.tools.handlers")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue