package scripts import ( "os" "strings" "testing" ) func TestDobotAIDingTalkComposeOwnsApplicationAndPostgres(t *testing.T) { compose, err := os.ReadFile("../deploy/dobotai-dingtalk/compose.yaml") if err != nil { t.Fatal(err) } for _, required := range []string{ "dobotai-dingtalk:", "container_name: dobotai-dingtalk", "dobotai-postgres:", "container_name: dobotai-postgres", "dobotai-postgres-data:", "depends_on:", "condition: service_healthy", "host.docker.internal:host-gateway", "127.0.0.1:8090:8090", "env_file:", "read_only: true", "/var/lib/dobotai/secrets", } { if !strings.Contains(string(compose), required) { t.Errorf("compose missing %q", required) } } configure, err := os.ReadFile("../deploy/dobotai-dingtalk/dobotai-dingtalk-configure") if err != nil { t.Fatal(err) } for _, required := range []string{"configure_env.py", "dobotai-dingtalk-start"} { if !strings.Contains(string(configure), required) { t.Errorf("configure command missing %q", required) } } bind, err := os.ReadFile("../deploy/dobotai-dingtalk/dobotai-dingtalk-bind-admin") if err != nil { t.Fatal(err) } for _, required := range []string{"docker exec -it dobotai-dingtalk", "/usr/local/bin/dobotai bootstrap-admin"} { if !strings.Contains(string(bind), required) { t.Errorf("bind command missing %q", required) } } } func TestDobotAIDingTalkCredentialConfigurationNeverPassesSecretsOnCommandLine(t *testing.T) { data, err := os.ReadFile("../deploy/dobotai-dingtalk/configure_env.py") if err != nil { t.Fatal(err) } source := string(data) for _, required := range []string{"getpass.getpass", "os.replace", "0o600", "DINGTALK_CLIENT_SECRET"} { if !strings.Contains(source, required) { t.Errorf("credential configurator missing %q", required) } } for _, forbidden := range []string{"subprocess.run([", "-v", "DINGTALK_CLIENT_SECRET="} { if strings.Contains(source, forbidden) { t.Errorf("credential configurator must not expose secret through process arguments: %q", forbidden) } } } func TestDobotAIDingTalkCredentialConfigurationExplainsEveryInputWithRedactedExamples(t *testing.T) { configurator, err := os.ReadFile("../deploy/dobotai-dingtalk/configure_env.py") if err != nil { t.Fatal(err) } documentation, err := os.ReadFile("../deploy/dobotai-dingtalk/README.md") if err != nil { t.Fatal(err) } if !strings.Contains(string(configurator), "格式示例:{example}") || !strings.Contains(string(documentation), "格式示例") { t.Fatal("configuration guidance must label every example") } for _, example := range []string{ "http://host.docker.internal:8080", `{"dingtalk:*":"你的Agent ID"}`, "DINGTALK_CLIENT_SECRET", } { if !strings.Contains(string(configurator), example) { t.Errorf("configurator missing redacted example %q", example) } if !strings.Contains(string(documentation), example) { t.Errorf("documentation missing redacted example %q", example) } } if strings.Contains(string(configurator), "DATABASE_URL") { t.Fatal("database URL must be deployment-managed, not requested interactively") } if !strings.Contains(string(documentation), "dobotai-postgres") { t.Fatal("documentation must explain the Compose-managed database") } } func TestDobotAIDingTalkStartRollsBackToPreviousContainerImage(t *testing.T) { data, err := os.ReadFile("../deploy/dobotai-dingtalk/dobotai-dingtalk-start") if err != nil { t.Fatal(err) } source := string(data) for _, required := range []string{ "previous_image=", "set_runtime_image", "docker inspect --format", "set_runtime_image \"$previous_image\"", "\"${compose[@]}\" up -d --no-build --force-recreate dobotai-dingtalk", } { if !strings.Contains(source, required) { t.Errorf("start script missing container upgrade rollback %q", required) } } } func TestDobotAIDingTalkDockerInstallerPreparesDirectoriesDatabaseAndMigrations(t *testing.T) { data, err := os.ReadFile("../deploy/dobotai-dingtalk/install-docker.sh") if err != nil { t.Fatal(err) } source := string(data) for _, required := range []string{ "container", "secrets", "backups", "migrations", "postgres.env", "dobotai-postgres", "apply-migrations.sh", "docker compose", "umask 077", } { if !strings.Contains(source, required) { t.Errorf("docker installer missing %q", required) } } if strings.Contains(source, "dobotai-dingtalk up -d") { t.Fatal("installer must prepare database but must not start the unconfigured application") } } func TestDobotAIDingTalkExistingDatabaseMigrationHasBackupAndRollbackBoundary(t *testing.T) { data, err := os.ReadFile("../deploy/dobotai-dingtalk/migrate-existing-database.sh") if err != nil { t.Fatal(err) } source := string(data) for _, required := range []string{ "pg_dump -Fc", "pg_restore --list", "dobotai-postgres-data", "dobotai-dingtalk", "rollback", "apply-migrations.sh", } { if !strings.Contains(source, required) { t.Errorf("database adoption script missing %q", required) } } } func TestRootDockerInstallerIsCloneAndRunEntryPoint(t *testing.T) { data, err := os.ReadFile("../install-docker.sh") if err != nil { t.Fatal(err) } source := string(data) for _, required := range []string{ "docker build", "dobotai-dingtalk:local-", "install_docker_if_missing", "docker compose version", "DOBOTAI_SOURCE_ROOT", "deploy/dobotai-dingtalk/install-docker.sh", } { if !strings.Contains(source, required) { t.Errorf("root installer missing %q", required) } } if strings.Contains(source, "${DOBOTAI_IMAGE") || strings.Contains(source, "DOBOTAI_IMAGE=?") { t.Fatal("root installer must not require an operator supplied DOBOTAI_IMAGE") } } func TestRootDockerInstallerBuildsWithInternalGoModuleProxy(t *testing.T) { installer, err := os.ReadFile("../install-docker.sh") if err != nil { t.Fatal(err) } dockerfile, err := os.ReadFile("../Dockerfile") if err != nil { t.Fatal(err) } if !strings.Contains(string(installer), "GOPROXY=https://goproxy.cn,direct") { t.Fatal("root installer must pass an internal default GOPROXY to docker build") } if !strings.Contains(string(dockerfile), "ARG GOPROXY") || !strings.Contains(string(dockerfile), "GOPROXY=$GOPROXY go mod download") { t.Fatal("Dockerfile must accept and use the root installer GOPROXY") } }