|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import tempfile |
| 4 | +import time |
1 | 5 |
|
| 6 | +# Ensure this directory is on sys.path so test files can import constants.py |
| 7 | +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
2 | 8 |
|
3 | 9 | import boto3 |
4 | | -import os |
5 | 10 | import pytest |
| 11 | +import requests |
| 12 | +from boto3.exceptions import S3UploadFailedError |
| 13 | +from botocore.exceptions import EndpointConnectionError |
| 14 | + |
| 15 | +from constants import BUCKETS |
| 16 | + |
| 17 | + |
| 18 | +# --------------------------------------------------------------------------- |
| 19 | +# Internal helpers (not fixtures) |
| 20 | +# --------------------------------------------------------------------------- |
| 21 | + |
| 22 | +def _delete_bucket(client, bucket: str) -> None: |
| 23 | + """Delete a bucket and all its objects.""" |
| 24 | + while True: |
| 25 | + objects = client.list_objects(Bucket=bucket) |
| 26 | + content = objects.get('Contents', []) |
| 27 | + if not content: |
| 28 | + break |
| 29 | + for obj in content: |
| 30 | + client.delete_object(Bucket=bucket, Key=obj['Key']) |
| 31 | + client.delete_bucket(Bucket=bucket) |
| 32 | + |
| 33 | + |
| 34 | +def _s3_healthcheck(client) -> None: |
| 35 | + """Retry S3 connectivity until backend is ready.""" |
| 36 | + bucket = 'artifacts-healthcheck' |
| 37 | + filename = tempfile.mktemp() |
| 38 | + with open(filename, 'wb') as fd: |
| 39 | + fd.write(os.urandom(1024)) |
| 40 | + try: |
| 41 | + for attempt in range(10): |
| 42 | + try: |
| 43 | + client.create_bucket(Bucket=bucket) |
| 44 | + client.upload_file(filename, bucket, filename) |
| 45 | + _delete_bucket(client, bucket) |
| 46 | + return |
| 47 | + except (S3UploadFailedError, EndpointConnectionError): |
| 48 | + time.sleep(attempt + 1) |
| 49 | + raise RuntimeError("S3 backend never became healthy") |
| 50 | + finally: |
| 51 | + os.remove(filename) |
6 | 52 |
|
7 | | -@pytest.fixture(scope="class") |
8 | | -def s3_client(request): |
| 53 | + |
| 54 | +# --------------------------------------------------------------------------- |
| 55 | +# Session-scoped fixtures (created once per pytest run) |
| 56 | +# --------------------------------------------------------------------------- |
| 57 | + |
| 58 | +@pytest.fixture(scope="session") |
| 59 | +def s3_client(): |
| 60 | + """Boto3 S3 client pointed at the test cloudserver backend.""" |
9 | 61 | session = boto3.session.Session() |
10 | | - s3_client = session.client( |
| 62 | + client = session.client( |
11 | 63 | service_name='s3', |
12 | 64 | aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID', 'accessKey1'), |
13 | 65 | aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY', 'verySecretKey1'), |
14 | | - endpoint_url=os.getenv('ENDPOINT_URL', 'http://cloudserver-front:8000') |
| 66 | + endpoint_url=os.getenv('ENDPOINT_URL', 'http://cloudserver-front:8000'), |
15 | 67 | ) |
16 | | - request.cls.s3_client = s3_client |
17 | | - |
18 | | -@pytest.fixture(scope="class") |
19 | | -def artifacts_url(request): |
20 | | - request.cls.artifacts_url = os.getenv('ARTIFACTS_URL', 'http://localhost') |
21 | | - |
22 | | -@pytest.fixture(scope="class") |
23 | | -def container(request): |
24 | | - request.cls.container = 'githost:owner:repo:staging-8e50acc6a1.pre-merge.28.1' |
25 | | - |
26 | | -@pytest.fixture(scope="class") |
27 | | -def buckets(request): |
28 | | - buckets = ( |
29 | | - 'artifacts-staging', |
30 | | - 'artifacts-promoted', |
31 | | - 'artifacts-prolonged' |
32 | | - ) |
33 | | - request.cls.buckets = buckets |
| 68 | + _s3_healthcheck(client) |
| 69 | + return client |
| 70 | + |
| 71 | + |
| 72 | +@pytest.fixture(scope="session") |
| 73 | +def artifacts_url() -> str: |
| 74 | + return os.getenv('ARTIFACTS_URL', 'http://artifacts') |
| 75 | + |
| 76 | + |
| 77 | +# --------------------------------------------------------------------------- |
| 78 | +# Function-scoped fixtures (created fresh for each test) |
| 79 | +# --------------------------------------------------------------------------- |
| 80 | + |
| 81 | +@pytest.fixture(autouse=True) |
| 82 | +def managed_buckets(s3_client): |
| 83 | + """Create the three artifact buckets before each test; destroy them after.""" |
| 84 | + for bucket in BUCKETS: |
| 85 | + s3_client.create_bucket(Bucket=bucket) |
| 86 | + yield |
| 87 | + for bucket in BUCKETS: |
| 88 | + _delete_bucket(s3_client, bucket) |
| 89 | + |
| 90 | + |
| 91 | +@pytest.fixture |
| 92 | +def session(): |
| 93 | + """Requests session authenticated as a user with full upload rights.""" |
| 94 | + s = requests.Session() |
| 95 | + s.auth = ('username-pass', 'fake-password') |
| 96 | + return s |
| 97 | + |
| 98 | + |
| 99 | +@pytest.fixture |
| 100 | +def restricted_session(): |
| 101 | + """Requests session authenticated as a read-only user (no upload/copy).""" |
| 102 | + s = requests.Session() |
| 103 | + s.auth = ('username-pass-no-restricted-paths', 'fake-password') |
| 104 | + return s |
| 105 | + |
| 106 | + |
| 107 | +@pytest.fixture |
| 108 | +def bot_session(): |
| 109 | + """Requests session using the local bot credentials.""" |
| 110 | + s = requests.Session() |
| 111 | + s.auth = ('botuser', 'botpass') |
| 112 | + return s |
| 113 | + |
| 114 | + |
| 115 | +@pytest.fixture |
| 116 | +def anon_session(): |
| 117 | + """Unauthenticated requests session.""" |
| 118 | + return requests.Session() |
| 119 | + |
| 120 | + |
| 121 | +# --------------------------------------------------------------------------- |
| 122 | +# Factory fixtures for common setup operations |
| 123 | +# --------------------------------------------------------------------------- |
| 124 | + |
| 125 | +@pytest.fixture |
| 126 | +def upload_file(session, artifacts_url): |
| 127 | + """Factory: upload bytes to ``/upload/<build>/<path>``.""" |
| 128 | + def _upload(build: str, path: str, data: bytes = b'test content') -> requests.Response: |
| 129 | + url = f'{artifacts_url}/upload/{build}/{path}' |
| 130 | + resp = session.put(url, data=data) |
| 131 | + assert resp.status_code == 200, f'upload {path}: {resp.status_code} {resp.text}' |
| 132 | + return resp |
| 133 | + return _upload |
| 134 | + |
| 135 | + |
| 136 | +@pytest.fixture |
| 137 | +def finish_build(session, artifacts_url): |
| 138 | + """Factory: mark a build finished by uploading ``.final_status``. |
| 139 | +
|
| 140 | + Also sends a ``ForceCacheUpdate`` GET to flush the nginx proxy cache for |
| 141 | + that object, so subsequent ``/copy/`` or ``/last_success/`` calls see it. |
| 142 | + """ |
| 143 | + def _finish(build: str, status: str = 'SUCCESSFUL') -> None: |
| 144 | + url = f'{artifacts_url}/upload/{build}/.final_status' |
| 145 | + resp = session.put(url, data=status.encode()) |
| 146 | + assert resp.status_code == 200 |
| 147 | + # Flush proxy cache |
| 148 | + session.get( |
| 149 | + f'{artifacts_url}/download/{build}/.final_status', |
| 150 | + headers={'ForceCacheUpdate': 'yes'}, |
| 151 | + ) |
| 152 | + return _finish |
0 commit comments