Files
Octop/tests/live/test_oss_storage.py
085506e3f3 Feat/connectors gateway install (#14)
* feat(connectors): add gateway connectors and drop baidu-netdisk/figma

Expand the catalog with QQ Music, Fliggy, Baidu Map, Ctrip Wendao, Meituan Travel, and Yuandian; migrate Tencent News to API key; unify gateway credential probes; remove Baidu Netdisk/Figma OAuth paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(install): ship Playwright by default and harden install scripts

Make browser tooling a core dependency, drop obsolete optional extras, and improve glibc/Playwright system-deps handling across install.sh/ps1/bat.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(desktop): install build deps before Python packages on Linux

Add a build-deps-only path so remote desktop setup can compile extension wheels when system headers are missing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dashboard): cache update-status checks in localStorage

Deduplicate concurrent PyPI probes across mounts and persist the last known update status with a short TTL.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(live): add OSS probes, secret helpers, and CI live job

Centralize require_env skipping, document live credentials in .env.example, and wire a secrets-gated live-tests workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(tests): rename chat WS integration test and drop dead cases

Align the chat transport test name with WebSocket usage and remove a skipped bootstrap harness test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): unblock typecheck, CodeQL, and drifted live tests

Align mypy with requires-python 3.12 so numpy stubs parse; clarify Fliggy
SHA-256 use for request signing; update live expert/backend assertions
to the lazy Expert catalog and DEFAULT_BACKEND_SPEC with root_dir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): news Skill-Request-Id, workspace live reads, CodeQL FP

Add Tencent News CLI headers required by error 4010; assert expert
seeds via agent.workspace instead of host-rooted backend paths; ignore
Fliggy signing digests in CodeQL (protocol SHA-256, not password KDF).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): allow newline-only __init__.py in expert live checks

Office-automation skill packages ship empty package markers; strip()-based
emptiness assertions should not treat those as copy failures.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(tests): compare expert seeds via adownload_bytes

Harness aread_text replaces empty/whitespace file bodies with a system
reminder, which broke live office-automation __init__.py checks.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 19:25:51 +08:00

111 lines
4.7 KiB
Python

"""Live round-trip tests against real object-storage backends (COS / S3 / OSS / OBS).
These exercise the *real* product code path:
* :func:`octop.infra.backend.probe.probe_storage_backend` is exactly what the
dashboard calls when a user adds / verifies a storage backend — so a green
probe means real credentials are accepted by the real mapping.
* A boto3 put/get/delete round-trip proves end-to-end read/write on the bucket.
Multiple backends are probed in a single run. Each backend reads its own
env-var *prefix*; when a backend's required vars are absent, ONLY that
backend's cases auto-skip, so CI without secrets stays green.
Prefixes (repo-root ``.env``, loaded by ``tests/live/conftest.py``):
OSS_ → Tencent COS (OSS_KIND=cos)
ALI_OSS_ → Aliyun OSS (ALI_OSS_KIND=oss)
Run locally::
uv run pytest tests/live/test_oss_storage.py -m live -v
"""
from __future__ import annotations
import uuid
import boto3
import pytest
from botocore.config import Config
from tests.support.secrets import optional_env, require_env
from octop.infra.backend.adapter import row_to_backend_spec
from octop.infra.backend.probe import probe_storage_backend, row_for_probe
pytestmark = pytest.mark.live
# Each backend reads its own env-var prefix. A backend is probed only when ALL
# of its required vars are present; otherwise only that backend's cases skip.
# `signature_version` selects the S3 sig scheme: Aliyun OSS rejects boto3's v4
# chunked ("aws-chunked") uploads, so it falls back to the V2 scheme; COS/S3 use
# the default v4 with a fully-signed body.
_BACKENDS: list[dict[str, str | None]] = [
{"label": "cos", "prefix": "OSS_", "default_kind": "cos", "signature_version": None},
{"label": "aliyun-oss", "prefix": "ALI_OSS_", "default_kind": "oss", "signature_version": "s3"},
]
def _load_backend(prefix: str, default_kind: str) -> object:
"""Build a BackendRow from a prefixed env; skip if credentials absent."""
endpoint = optional_env(f"{prefix}ENDPOINT")
access_key = optional_env(f"{prefix}ACCESS_KEY")
secret_key = optional_env(f"{prefix}SECRET_KEY")
bucket = optional_env(f"{prefix}BUCKET")
if not (endpoint and access_key and secret_key and bucket):
pytest.skip(f"set {prefix}ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET to run")
return row_for_probe(
kind=optional_env(f"{prefix}KIND", default_kind) or default_kind,
endpoint=endpoint,
access_key=access_key,
secret_key=secret_key,
bucket=bucket,
region=optional_env(f"{prefix}REGION"),
)
@pytest.mark.parametrize("backend", _BACKENDS, ids=lambda b: b["label"])
def test_oss_probe_accepts_real_credentials(backend: dict[str, str]) -> None:
"""The real backend probe must succeed with valid credentials."""
result = probe_storage_backend(_load_backend(backend["prefix"], backend["default_kind"]))
assert result.get("ok") is True, result
@pytest.mark.parametrize("backend", _BACKENDS, ids=lambda b: b["label"])
def test_oss_spec_mapping_is_complete(backend: dict[str, str]) -> None:
"""The DB row must map to a non-empty harness backend spec."""
spec = row_to_backend_spec(_load_backend(backend["prefix"], backend["default_kind"]))
assert spec is not None
assert spec.get("type")
assert spec.get("bucket")
@pytest.mark.parametrize("backend", _BACKENDS, ids=lambda b: b["label"])
def test_oss_roundtrip_put_get_delete(backend: dict[str, str]) -> None:
"""End-to-end write/read/delete on the real bucket (auto-cleaned)."""
prefix = backend["prefix"]
endpoint = require_env(f"{prefix}ENDPOINT")
bucket = require_env(f"{prefix}BUCKET")
key_prefix = optional_env(f"{prefix}PREFIX", "").strip().lstrip("/")
key = f"{key_prefix}octop-live-test/{uuid.uuid4().hex}/hello.txt"
sig = backend.get("signature_version")
s3_cfg: dict[str, object] = {"addressing_style": "virtual"}
# Aliyun OSS rejects boto3's v4 chunked ("aws-chunked") uploads; the V2
# scheme avoids chunked encoding entirely and is accepted by Aliyun.
if sig is None:
s3_cfg["payload_signature_version"] = "s3v4"
client = boto3.client(
"s3",
endpoint_url=endpoint if "://" in endpoint else f"https://{endpoint}",
aws_access_key_id=require_env(f"{prefix}ACCESS_KEY"),
aws_secret_access_key=require_env(f"{prefix}SECRET_KEY"),
region_name=optional_env(f"{prefix}REGION") or None,
config=Config(signature_version=sig, s3=s3_cfg),
)
client.put_object(Bucket=bucket, Key=key, Body=b"ping")
try:
body = client.get_object(Bucket=bucket, Key=key)["Body"].read()
assert body == b"ping"
finally:
client.delete_object(Bucket=bucket, Key=key)