-
Notifications
You must be signed in to change notification settings - Fork 0
feat: validacao com testcontainers, build docker no ci e retencao de pacotes #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| """Validação da imagem Docker do 9RTKSync usando Testcontainers. | ||
|
|
||
| Garante que a imagem Docker gerada: | ||
| 1. Inicializa sem falhas com as configurações padrão. | ||
| 2. Expõe o dashboard web na porta 9090. | ||
| 3. Responde requisições HTTP em /login com código 200 e página HTML válida. | ||
| 4. Responde no endpoint /healthz com o cabeçalho e corpo documentados. | ||
| 5. Permite executar a CLI (9rtksync --help) com sucesso dentro do container. | ||
| """ | ||
|
|
||
| import os | ||
| import time | ||
| import unittest | ||
| import urllib.request | ||
| import urllib.error | ||
|
|
||
| RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| DOCKER_IMAGE_PADRAO = "9rtksync:test" | ||
|
|
||
|
|
||
| def docker_disponivel() -> bool: | ||
| try: | ||
| import docker | ||
| cliente = docker.from_env() | ||
| cliente.ping() | ||
| return True | ||
| except Exception: | ||
| return False | ||
|
|
||
|
|
||
| def obter_ou_construir_imagem(nome_imagem: str) -> str: | ||
| import docker | ||
|
|
||
| cliente = docker.from_env() | ||
| imagens = cliente.images.list(name=nome_imagem) | ||
| if imagens: | ||
| return nome_imagem | ||
|
|
||
| # Constrói localmente se não existir pré-construída | ||
| dockerfile = os.path.join(RAIZ, "Dockerfile") | ||
| if os.path.exists(dockerfile): | ||
| imagem, _ = cliente.images.build(path=RAIZ, tag=nome_imagem, rm=True) | ||
| return nome_imagem | ||
|
|
||
| raise RuntimeError(f"Imagem {nome_imagem} não encontrada e Dockerfile não localizado em {RAIZ}") | ||
|
|
||
|
|
||
| class Test9RTKSyncContainer(unittest.TestCase): | ||
| @classmethod | ||
| def setUpClass(cls): | ||
| try: | ||
| import testcontainers | ||
| import docker | ||
| except ImportError: | ||
| raise unittest.SkipTest("testcontainers ou docker não estão instalados") | ||
|
|
||
| if not docker_disponivel(): | ||
| raise unittest.SkipTest("Docker daemon não está disponível ou acessível") | ||
|
|
||
| cls.imagem = os.environ.get("TEST_DOCKER_IMAGE", DOCKER_IMAGE_PADRAO) | ||
| try: | ||
| cls.imagem = obter_ou_construir_imagem(cls.imagem) | ||
| except Exception as e: | ||
| raise unittest.SkipTest(f"Falha ao obter ou construir imagem Docker: {e}") | ||
|
Comment on lines
+62
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Docker is available but the requested image is absent, this helper attempts to build the Dockerfile; however, any build error is converted into Useful? React with 👍 / 👎. |
||
|
|
||
| def test_container_web_dashboard_e_cli(self): | ||
| from testcontainers.core.container import DockerContainer | ||
|
|
||
| with DockerContainer(self.imagem).with_exposed_ports(9090) as container: | ||
| host = container.get_container_host_ip() | ||
| porta = container.get_exposed_port(9090) | ||
| url_login = f"http://{host}:{porta}/login" | ||
| url_healthz = f"http://{host}:{porta}/healthz" | ||
|
|
||
| # 1. Aguarda o servidor web aceitar conexões (até 15s) | ||
| conectou = False | ||
| ultimo_erro = None | ||
| for _ in range(15): | ||
| time.sleep(1) | ||
| try: | ||
| with urllib.request.urlopen(url_login, timeout=2) as resp: | ||
| if resp.status == 200: | ||
| conectou = True | ||
| break | ||
| except Exception as e: | ||
| ultimo_erro = e | ||
|
|
||
| self.assertTrue(conectou, f"Não foi possível conectar ao dashboard web na porta {porta}: {ultimo_erro}") | ||
|
|
||
| # 2. Valida resposta do endpoint /login | ||
| with urllib.request.urlopen(url_login, timeout=5) as resp: | ||
| self.assertEqual(resp.status, 200) | ||
| cabecalhos = dict(resp.getheaders()) | ||
| self.assertIn("text/html", cabecalhos.get("Content-Type", "")) | ||
| corpo = resp.read().decode("utf-8") | ||
| self.assertIn("9RTKSync", corpo) | ||
| self.assertIn("<!DOCTYPE html>", corpo) | ||
|
|
||
| # 3. Valida resposta do endpoint /healthz | ||
| try: | ||
| with urllib.request.urlopen(url_healthz, timeout=5) as resp: | ||
| status_healthz = resp.status | ||
| corpo_healthz = resp.read().decode("utf-8") | ||
| cabecalho_server = resp.headers.get("Server", "") | ||
| except urllib.error.HTTPError as e: | ||
| status_healthz = e.code | ||
| corpo_healthz = e.read().decode("utf-8") | ||
| cabecalho_server = e.headers.get("Server", "") | ||
|
|
||
| # Sem banco montado e sem gateway externo, o healthz esperado é 503 DATABASE_NOT_READY | ||
| self.assertIn(status_healthz, (200, 503)) | ||
| self.assertEqual(cabecalho_server, "9RTKSync") | ||
| self.assertTrue( | ||
| "OK" in corpo_healthz or "DATABASE_NOT_READY" in corpo_healthz or "ROUTER_SERVICE_UNREACHABLE" in corpo_healthz, | ||
|
Comment on lines
+111
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The container started here has neither Useful? React with 👍 / 👎. |
||
| f"Corpo inesperado do healthz: {corpo_healthz}", | ||
| ) | ||
|
|
||
| # 4. Valida execução da CLI dentro do container | ||
| codigo_saida, saida = container.exec(["9rtksync", "--help"]) | ||
| self.assertEqual(codigo_saida, 0, f"Comando 9rtksync --help falhou com código {codigo_saida}: {saida}") | ||
| texto_saida = saida.decode("utf-8") | ||
| self.assertIn("9RTKSync", texto_saida) | ||
| self.assertIn("--daemon", texto_saida) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this duplicate retention job:
.github/workflows/cleanup-packages.ymlalready listens for completion ofRelease and Docker Packageand runs the same cleanup action with the same policy. Every successful release therefore performs deletion and registry validation twice, while a transient failure in this new job marks an otherwise successfully published release workflow as failed; if it fails, the existing retention workflow then skips its cleanup because it requires a successful release conclusion.Useful? React with 👍 / 👎.