hy-app-monitor/tests/test_service_release.py

342 lines
15 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import patch
from core.config import HARBOR_PROJECT, HARBOR_REGISTRY, load_hosts
from monitors.yumi.runtime_config import (
GO_ENV_PRIMARY_BY_KEY,
GO_ENV_SPECS_BY_PRIMARY,
extract_go_values,
expand_host_updates,
validate_go_editor_content,
)
from monitors.yumi.service_release import build_update_targets_payload, deploy_updated_services_payload
from monitors.yumi.service_ops import service_records_by_name
class ServiceReleaseTargetsTests(unittest.TestCase):
def test_yumi_activity_runtime_envs_are_editable_with_strict_types(self) -> None:
expected_keys = {
"CHATAPP_JAVA_ORDER_BASE_URL",
"CHATAPP_YUMI_GIFT_CHALLENGE_DEFAULT_SYS_ORIGIN",
"CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENABLED",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENDPOINT",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_NAMESPACE",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ACCESS_KEY",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ACCESS_SECRET",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_SECURITY_TOKEN",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_CONSUMER_GROUP",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TOPIC",
"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TAG",
"CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED",
}
self.assertTrue(expected_keys.issubset(GO_ENV_SPECS_BY_PRIMARY))
self.assertEqual(
GO_ENV_PRIMARY_BY_KEY["GAME_INTERNAL_CALLBACK_SECRET"],
"CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET",
)
self.assertEqual(
GO_ENV_PRIMARY_BY_KEY["GAME_JAVA_ORDER_BASE_URL"],
"CHATAPP_JAVA_ORDER_BASE_URL",
)
valid = validate_go_editor_content(
"\n".join(
[
'CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET="shared-secret"',
'GAME_JAVA_ORDER_BASE_URL="http://10.2.1.15:2600"',
'CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS="300"',
'CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENABLED="true"',
'CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENDPOINT="rocketmq.example:8081"',
'CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED="false"',
]
)
)
self.assertTrue(valid["ok"])
self.assertEqual(valid["values"]["CHATAPP_JAVA_ORDER_BASE_URL"], "http://10.2.1.15:2600")
self.assertEqual(valid["values"]["CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED"], "false")
self.assertEqual(
extract_go_values({})["CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED"],
"false",
)
invalid_lease = validate_go_editor_content(
'CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS="0"'
)
self.assertFalse(invalid_lease["ok"])
self.assertIn("positive integer", invalid_lease["message"])
invalid_enabled = validate_go_editor_content(
'CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENABLED="enabled"'
)
self.assertFalse(invalid_enabled["ok"])
self.assertIn("must be a boolean", invalid_enabled["message"])
def test_game_king_legacy_env_shape_is_preserved_when_saved(self) -> None:
updates = expand_host_updates(
{
"GAME_INTERNAL_CALLBACK_SECRET": "old-secret",
"GAME_JAVA_ORDER_BASE_URL": "http://old-order:2600",
},
{
"CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET": "new-secret",
"CHATAPP_JAVA_ORDER_BASE_URL": "http://new-order:2600",
},
)
self.assertEqual(updates["GAME_INTERNAL_CALLBACK_SECRET"], "new-secret")
self.assertEqual(updates["GAME_JAVA_ORDER_BASE_URL"], "http://new-order:2600")
self.assertNotIn("CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET", updates)
self.assertNotIn("CHATAPP_JAVA_ORDER_BASE_URL", updates)
def test_runtime_topology_keeps_singapore_cron_out_of_golang_release(self) -> None:
hosts = load_hosts()
cron_hosts = [host for host in hosts if host["host"] == "cron-sg-1"]
records = service_records_by_name().get("golang") or []
self.assertEqual(len(cron_hosts), 1)
self.assertEqual(cron_hosts[0]["ip"], "10.3.1.14")
self.assertEqual(cron_hosts[0]["instanceId"], "ins-nt6bsay0")
self.assertEqual(cron_hosts[0]["group"], "cron")
self.assertEqual(cron_hosts[0]["services"], [])
self.assertFalse(any(record["host"] == "cron-sg-1" for record in records))
@patch("monitors.yumi.service_release.gateway_clb_configuration_error", return_value="gateway CLB protection requires .env")
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
@patch("monitors.yumi.service_release.service_repository_ref", side_effect=lambda service: f"repo/{service}")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["gateway"])
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.load_local_host_compose_model")
def test_build_update_targets_payload_adds_gateway_clb_warning(
self,
load_local_host_compose_model_mock,
service_records_by_name_mock,
buildable_service_names_mock,
service_repository_ref_mock,
frontend_release_enabled_mock,
gateway_clb_configuration_error_mock,
) -> None:
del buildable_service_names_mock
del service_repository_ref_mock
del frontend_release_enabled_mock
del gateway_clb_configuration_error_mock
service_records_by_name_mock.return_value = {
"gateway": [
{
"host": "gateway-1",
"ip": "10.0.0.1",
"instanceId": "ins-gateway-1",
"port": 8080,
"path": "/health",
}
]
}
load_local_host_compose_model_mock.return_value = {
"services": {
"gateway": {"image": "registry/gateway:main"},
}
}
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(payload["items"][0]["warnings"], ["gateway CLB protection requires .env"])
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
@patch("monitors.yumi.service_release.service_repository_ref", side_effect=lambda service: f"repo/{service}")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth", "gateway"])
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.load_local_host_compose_model")
def test_build_update_targets_payload_reuses_host_compose_model(
self,
load_local_host_compose_model_mock,
service_records_by_name_mock,
buildable_service_names_mock,
service_repository_ref_mock,
frontend_release_enabled_mock,
) -> None:
del buildable_service_names_mock, service_repository_ref_mock, frontend_release_enabled_mock
service_records_by_name_mock.return_value = {
"auth": [
{
"host": "app-1",
"ip": "10.0.0.1",
"instanceId": "ins-app-1",
"port": 8080,
"path": "/auth",
}
],
"gateway": [
{
"host": "app-1",
"ip": "10.0.0.1",
"instanceId": "ins-app-1",
"port": 9000,
"path": "/gateway",
}
],
}
load_local_host_compose_model_mock.return_value = {
"services": {
"auth": {"image": "registry/auth:main"},
"gateway": {"image": "registry/gateway:main"},
}
}
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(load_local_host_compose_model_mock.call_count, 1)
self.assertEqual([item["service"] for item in payload["items"]], ["auth", "gateway"])
self.assertEqual(payload["items"][0]["currentImages"], ["registry/auth:main"])
self.assertEqual(payload["items"][1]["currentImages"], ["registry/gateway:main"])
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
@patch("monitors.yumi.service_release.service_repository_ref", side_effect=lambda service: f"repo/{service}")
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth"])
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.load_local_host_compose_model", side_effect=RuntimeError("broken compose"))
def test_build_update_targets_payload_keeps_items_when_compose_unavailable(
self,
load_local_host_compose_model_mock,
service_records_by_name_mock,
buildable_service_names_mock,
service_repository_ref_mock,
frontend_release_enabled_mock,
) -> None:
del load_local_host_compose_model_mock
del buildable_service_names_mock, service_repository_ref_mock, frontend_release_enabled_mock
service_records_by_name_mock.return_value = {
"auth": [
{
"host": "app-1",
"ip": "10.0.0.1",
"instanceId": "ins-app-1",
"port": 8080,
"path": "/auth",
}
]
}
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["items"]), 1)
self.assertEqual(payload["items"][0]["currentImages"], [])
self.assertEqual(payload["items"][0]["warnings"], ["app-1: broken compose"])
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth"])
@patch("monitors.yumi.service_release.service_records_by_name")
@patch("monitors.yumi.service_release.load_local_host_compose_model", side_effect=RuntimeError("broken compose"))
def test_build_update_targets_payload_falls_back_repository_when_compose_unavailable(
self,
load_local_host_compose_model_mock,
service_records_by_name_mock,
buildable_service_names_mock,
frontend_release_enabled_mock,
) -> None:
del load_local_host_compose_model_mock
del buildable_service_names_mock, frontend_release_enabled_mock
service_records_by_name_mock.return_value = {
"auth": [
{
"host": "app-1",
"ip": "10.0.0.1",
"instanceId": "ins-app-1",
"port": 8080,
"path": "/auth",
}
]
}
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["items"]), 1)
self.assertEqual(payload["items"][0]["repository"], f"{HARBOR_REGISTRY}/{HARBOR_PROJECT}/java/chatapp3-auth")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_INSTANCE_ID", "ins-admin")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_IP", "10.2.1.2")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_HOST", "admin-platform")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_REPO_URL", "git@example.com:hy/chatapp3-admin.git")
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_SITE_DIR", "/opt/yumi-admin/frontend-admin/site/yumiadmin-acc")
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
@patch(
"monitors.yumi.service_release.frontend_release_display",
return_value={
"currentLabel": "main@abc123",
"currentTitle": "abc123",
"detailLabel": "abc123",
"detailTitle": "abc123",
"copyValue": "abc123",
},
)
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["admin"])
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
def test_build_update_targets_payload_adds_admin_frontend_target(
self,
service_records_by_name_mock,
buildable_service_names_mock,
frontend_release_display_mock,
frontend_release_enabled_mock,
) -> None:
del service_records_by_name_mock, buildable_service_names_mock
del frontend_release_display_mock, frontend_release_enabled_mock
payload = build_update_targets_payload()
self.assertTrue(payload["ok"])
self.assertEqual(len(payload["items"]), 1)
admin = payload["items"][0]
self.assertEqual(admin["service"], "admin")
self.assertEqual(admin["kind"], "frontend")
self.assertEqual(admin["repository"], "git@example.com:hy/chatapp3-admin.git")
self.assertEqual(admin["hosts"][0]["host"], "admin-platform")
self.assertEqual(admin["hosts"][0]["ip"], "10.2.1.2")
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
@patch(
"monitors.yumi.service_release.deploy_frontend_site",
return_value={
"ok": True,
"updatedAt": "2026-04-24T00:00:00+00:00",
"services": ["admin"],
"build": [{"kind": "frontend", "gitRef": "release/admin", "commit": "abc123"}],
"templates": [],
"preparedHosts": [],
"steps": [{"service": "admin", "host": "admin-platform", "status": "success"}],
"message": "service update complete",
},
)
def test_deploy_updated_services_payload_dispatches_admin_frontend(
self,
deploy_frontend_site_mock,
service_records_by_name_mock,
frontend_release_enabled_mock,
) -> None:
del service_records_by_name_mock, frontend_release_enabled_mock
payload = deploy_updated_services_payload(
["admin"],
java_git_ref="main",
go_git_ref="main",
admin_git_ref="release/admin",
image_tag="",
skip_maven_build=False,
)
deploy_frontend_site_mock.assert_called_once_with("admin", git_ref="release/admin", progress=None)
self.assertTrue(payload["ok"])
self.assertEqual(payload["services"], ["admin"])
self.assertEqual(payload["build"][0]["kind"], "frontend")
self.assertEqual(payload["steps"][0]["service"], "admin")
if __name__ == "__main__":
unittest.main()