feat: complete contract-driven business development and MySQL integration
This commit is contained in:
76
scripts/testdata/mysqlGenerated_test.go
vendored
Normal file
76
scripts/testdata/mysqlGenerated_test.go
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"example.com/mysqlAcceptance/internal/config"
|
||||
"example.com/mysqlAcceptance/internal/svc"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
)
|
||||
|
||||
func TestGeneratedMySQLConfigurationAndLifecycle(t *testing.T) {
|
||||
cfg, err := config.Load("etc/config.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Database.User != "omni_test" || cfg.Database.TLS.ServerName != "localhost" || cfg.Database.Pool.MaxOpenConns != 2 {
|
||||
t.Fatal("structured environment overlay was not applied")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
var logs bytes.Buffer
|
||||
logger, err := logx.New(logx.Config{Writer: &logs})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
first, err := svc.New(ctx, logger, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer first.Close()
|
||||
second, err := svc.New(ctx, logger, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer second.Close()
|
||||
if first.DB == second.DB {
|
||||
t.Fatal("contexts share a pool")
|
||||
}
|
||||
if err = first.DB.Health(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var key, cipher string
|
||||
if err = first.DB.QueryRowContext(ctx, "SHOW SESSION STATUS LIKE 'Ssl_cipher'").Scan(&key, &cipher); err != nil || cipher == "" {
|
||||
t.Fatal("generated pool has no TLS", err)
|
||||
}
|
||||
if err = first.Finish(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = first.Finish(nil); err != nil {
|
||||
t.Fatal("cleanup not idempotent", err)
|
||||
}
|
||||
if err = first.DB.PingContext(ctx); err == nil {
|
||||
t.Fatal("dependency pool remains open")
|
||||
}
|
||||
if err = second.DB.Health(ctx); err != nil {
|
||||
t.Fatal("another context was closed", err)
|
||||
}
|
||||
bad := cfg
|
||||
bad.Database.TLS.ServerName = "wrong.invalid"
|
||||
failed, err := svc.New(ctx, logger, bad)
|
||||
var verify *tls.CertificateVerificationError
|
||||
if failed != nil || !errors.As(err, &verify) {
|
||||
t.Fatal("TLS failure not propagated", err)
|
||||
}
|
||||
logger.Info("logger remains usable")
|
||||
if !strings.Contains(logs.String(), "logger remains usable") {
|
||||
t.Fatal("borrowed logger was closed")
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import uuid
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE = "git.yuntuo.hk/k2safe/go-omni"
|
||||
SOURCES = ("database/connector_test.go", "database/mysqlConnector_test.go",
|
||||
"app/httpConfig_test.go", "config/http_test.go")
|
||||
"app/httpConfig_test.go", "config/http_test.go", "database/mysql/integration_test.go")
|
||||
|
||||
|
||||
def run(args, cwd, env, timeout=300):
|
||||
@@ -28,7 +28,7 @@ def run(args, cwd, env, timeout=300):
|
||||
text=True, capture_output=True, timeout=timeout)
|
||||
output = re.sub(r"https://[^/@\s]+:[^/@\s]+@", "https://", result.stdout + result.stderr)
|
||||
# Never print generated database credentials, including from a failed command.
|
||||
for key in ("MYSQL_ROOT_PASSWORD", "MYSQL_PASSWORD", "GO_OMNI_CONNECTOR_PASSWORD"):
|
||||
for key in ("MYSQL_ROOT_PASSWORD", "MYSQL_PASSWORD", "GO_OMNI_CONNECTOR_PASSWORD", "GO_OMNI_DB_PASSWORD"):
|
||||
if env.get(key):
|
||||
output = output.replace(env[key], "[redacted]")
|
||||
if result.returncode:
|
||||
@@ -62,12 +62,15 @@ def main():
|
||||
run(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", certs / "server.key", "-out", temp / "server.csr", "-subj", "/CN=localhost"], temp, env)
|
||||
(temp / "extensions.cnf").write_text("subjectAltName=DNS:localhost,IP:127.0.0.1\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth\n")
|
||||
run(["openssl", "x509", "-req", "-in", temp / "server.csr", "-CA", certs / "ca.pem", "-CAkey", certs / "ca.key", "-CAcreateserial", "-out", certs / "server.pem", "-days", "2", "-extfile", temp / "extensions.cnf"], temp, env)
|
||||
run(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", certs / "client.key", "-out", temp / "client.csr", "-subj", "/CN=GoOmni Test Client"], temp, env)
|
||||
(temp / "clientExtensions.cnf").write_text("basicConstraints=CA:FALSE\nextendedKeyUsage=clientAuth\n")
|
||||
run(["openssl", "x509", "-req", "-in", temp / "client.csr", "-CA", certs / "ca.pem", "-CAkey", certs / "ca.key", "-CAcreateserial", "-out", certs / "client.pem", "-days", "2", "-extfile", temp / "clientExtensions.cnf"], temp, env)
|
||||
(certs / "ca.key").unlink()
|
||||
for path in certs.iterdir():
|
||||
path.chmod(0o644) # Disposable fixtures, readable by the container's mysql uid.
|
||||
project = temp / "consumer"
|
||||
project.mkdir()
|
||||
version = args.version or "v0.9.1-preview"
|
||||
version = args.version or "v0.14.0-preview"
|
||||
(project / "go.mod").write_text(f"module example.com/connectorAcceptance\n\ngo 1.22.0\n\nrequire {MODULE} {version}\n")
|
||||
if args.local:
|
||||
run(["go", "mod", "edit", f"-replace={MODULE}={ROOT}"], project, env)
|
||||
@@ -75,13 +78,30 @@ def main():
|
||||
env["GOMODCACHE"] = str(temp / "modcache")
|
||||
for name in SOURCES:
|
||||
target = project / name
|
||||
target.parent.mkdir(exist_ok=True)
|
||||
target.parent.mkdir(exist_ok=True, parents=True)
|
||||
shutil.copyfile(ROOT / name, target)
|
||||
print("Resolving consumer dependency", flush=True)
|
||||
run(["go", "mod", "tidy"], project, env)
|
||||
module = json.loads(run(["go", "list", "-m", "-json", MODULE], project, env))
|
||||
if not args.local:
|
||||
assert module["Version"] == args.version and module.get("Sum") and not module.get("Replace"), module
|
||||
cli = temp / "bin/go-omni"
|
||||
cli.parent.mkdir()
|
||||
if args.local:
|
||||
run(["go", "build", "-o", cli, "./cmd/go-omni"], ROOT, env)
|
||||
else:
|
||||
env["GOBIN"] = str(cli.parent)
|
||||
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env)
|
||||
generated = temp / "generated"
|
||||
command = [cli, "new", "--database", "mysql", "--module", "example.com/mysqlAcceptance"]
|
||||
if args.local:
|
||||
command += ["--framework-dir", ROOT]
|
||||
run([*command, generated], temp, env)
|
||||
shutil.copyfile(ROOT / "scripts/testdata/mysqlGenerated_test.go", generated / "mysql_test.go")
|
||||
run(["go", "mod", "tidy"], generated, env)
|
||||
generated_module = json.loads(run(["go", "list", "-m", "-json", MODULE], generated, env))
|
||||
if not args.local:
|
||||
assert generated_module["Version"] == args.version and generated_module.get("Sum") == module["Sum"] and not generated_module.get("Replace")
|
||||
env.update(MYSQL_ROOT_PASSWORD=secrets.token_hex(24), MYSQL_PASSWORD=secrets.token_hex(24))
|
||||
name = "go-omni-connector-" + uuid.uuid4().hex[:12]
|
||||
created = False
|
||||
@@ -103,17 +123,34 @@ def main():
|
||||
time.sleep(1)
|
||||
if not ready:
|
||||
raise RuntimeError("isolated MySQL fixture did not become ready")
|
||||
admin_env = dict(env, MYSQL_PWD=env["MYSQL_ROOT_PASSWORD"])
|
||||
run(["docker", "exec", "--env", "MYSQL_PWD", name, "mysql", "-uroot", "-e", "CREATE USER 'omni_mtls'@'%' REQUIRE X509"], temp, admin_env)
|
||||
addr = run(["docker", "port", name, "3306/tcp"], temp, env).strip()
|
||||
assert addr.startswith("127.0.0.1:"), addr
|
||||
env.update(GO_OMNI_CONNECTOR_MYSQL_TEST="1", GO_OMNI_CONNECTOR_ADDR=addr,
|
||||
GO_OMNI_CONNECTOR_USER="omni_test", GO_OMNI_CONNECTOR_PASSWORD=env["MYSQL_PASSWORD"],
|
||||
GO_OMNI_CONNECTOR_DATABASE="omni_test", GO_OMNI_CONNECTOR_CA_FILE=str(certs / "ca.pem"))
|
||||
GO_OMNI_CONNECTOR_DATABASE="omni_test", GO_OMNI_CONNECTOR_CA_FILE=str(certs / "ca.pem"),
|
||||
GO_OMNI_CONNECTOR_CLIENT_CERT=str(certs / "client.pem"), GO_OMNI_CONNECTOR_CLIENT_KEY=str(certs / "client.key"))
|
||||
print("Testing TLS trust, per-connector options, driver logging, lifecycle and HTTP limits", flush=True)
|
||||
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=60s", "./..."], project, env)
|
||||
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||
passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e]
|
||||
assert "TestMySQLConnectorTLSAndPoolIsolation" in passed
|
||||
assert "TestStructuredMySQLAdapterTLS" in passed
|
||||
assert not any(e.get("Action") in ("fail", "skip") for e in events), output
|
||||
env.update(GO_OMNI_DB_HOST="127.0.0.1", GO_OMNI_DB_PORT=addr.rsplit(":", 1)[1],
|
||||
GO_OMNI_DB_USER="omni_test", GO_OMNI_DB_PASSWORD=env["MYSQL_PASSWORD"],
|
||||
GO_OMNI_DB_DATABASE="omni_test", GO_OMNI_DB_TLS_CA_FILE=str(certs / "ca.pem"),
|
||||
GO_OMNI_DB_TLS_SERVER_NAME="localhost", GO_OMNI_DB_POOL_MAX_OPEN_CONNS="2",
|
||||
GO_OMNI_DB_POOL_MAX_IDLE_CONNS="2")
|
||||
print("Testing generated MySQL service config, independent pools and cleanup", flush=True)
|
||||
run([cli, "api", "generate", "--check"], generated, env)
|
||||
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=60s", "./..."], generated, env)
|
||||
generated_events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||
generated_passed = [e["Test"] for e in generated_events if e.get("Action") == "pass" and "Test" in e]
|
||||
assert "TestGeneratedMySQLConfigurationAndLifecycle" in generated_passed, output
|
||||
assert not any(e.get("Action") == "fail" or (e.get("Action") == "skip" and "Test" in e) for e in generated_events), output
|
||||
passed += generated_passed
|
||||
image = run(["docker", "inspect", "--format", "{{.Image}}", name], temp, env).strip()
|
||||
finally:
|
||||
if created:
|
||||
@@ -123,7 +160,7 @@ def main():
|
||||
"noLocalReplace": not bool(module.get("Replace")), "mysqlImage": image,
|
||||
"mysqlLimits": {"cpus": 1, "memoryMiB": 768, "pids": 128},
|
||||
"durationSeconds": round(time.monotonic() - began, 2), "tests": passed,
|
||||
"sourceHashes": {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in (*SOURCES, "scripts/verifyConnector.py")}}
|
||||
"sourceHashes": {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in (*SOURCES, "scripts/verifyConnector.py", "scripts/testdata/mysqlGenerated_test.go")}}
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
print("PASS; isolated container, credentials, certificates and consumer removed", flush=True)
|
||||
|
||||
@@ -72,7 +72,7 @@ def main():
|
||||
checks += ["default404And405", "goOwnedErrorCatalog", "unwrappedDownloads", "rawCallback", "multipart400And413", "nethttpAndNativeTransfers"]
|
||||
|
||||
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
|
||||
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer"]
|
||||
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject"]
|
||||
print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True)
|
||||
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env)
|
||||
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||
@@ -81,10 +81,11 @@ def main():
|
||||
evidence = "".join(e.get("Output", "") for e in events)
|
||||
sums = re.findall(r"checksum (h1:\S+); no local replace", evidence)
|
||||
assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch"
|
||||
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup"]
|
||||
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "strictRequestPolicies", "declarativeMultipartBinding"]
|
||||
|
||||
sources = ["lifecycle/controller.go", "config/endpoints.go", "health/health.go", "httpx/response.go", "app/app.go", "app/transport.go", "scripts/verifyManagedRelease.py", "scripts/verifyManagedProject.py", "cmd/go-omni/apigen/managed_test.go"]
|
||||
sources = ["lifecycle/runtime.go", "config/project.go", "contract/bind.go", "contract/policy.go", "contract/multipart.go", "contract/file.go", "cmd/go-omni/apigen/business_test.go", "cmd/go-omni/apigen/generate.go", "lifecycle/controller.go", "config/endpoints.go", "health/health.go", "httpx/response.go", "app/app.go", "app/transport.go", "scripts/verifyManagedRelease.py", "scripts/verifyManagedProject.py", "cmd/go-omni/apigen/managed_test.go"]
|
||||
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/managed").glob("*.go")]
|
||||
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/business").glob("*.go")]
|
||||
report = {"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
|
||||
"freshModuleCache": True, "noLocalReplace": True,
|
||||
"durationSeconds": round(time.monotonic() - started, 2), "checks": checks,
|
||||
|
||||
Reference in New Issue
Block a user