mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
* mirror: Add FEATURE_ORG_MIRROR feature flag (PROJQUAY-1266) Add organization-level repository mirroring feature flag to enable the new org mirroring functionality. Feature is disabled by default. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * mirror: Add GET endpoint for org mirror config (PROJQUAY-1266) Implements the GET /v1/organization/<org>/mirror endpoint to retrieve organization-level mirror configuration. Includes business logic layer with get_org_mirror_config() and comprehensive unit tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * mirror: Add POST endpoint for org mirror config (PROJQUAY-1266) Add create endpoint for organization-level mirror configuration: - POST /v1/organization/<orgname>/mirror creates new config - Validates robot account ownership and credentials - Returns 201 on success, 409 if config already exists Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * mirror: Add DELETE endpoint for org mirror config (PROJQUAY-1266) Add delete endpoint for organization-level mirror configuration: - DELETE /v1/organization/<orgname>/mirror removes config - Also deletes all associated discovered repositories - Returns 204 on success, 404 if config doesn't exist Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * mirror: Add PUT endpoint for org mirror config (PROJQUAY-1266) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix test failure --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
941 lines
38 KiB
Python
941 lines
38 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Unit tests for organization-level mirror API endpoints.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
from data import model
|
|
from data.database import OrgMirrorConfig as OrgMirrorConfigModel
|
|
from endpoints.api import org_mirror
|
|
from endpoints.api.test.shared import conduct_api_call
|
|
from endpoints.test.shared import client_with_identity
|
|
from test.fixtures import *
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _cleanup_org_mirror_config(orgname):
|
|
"""Helper to clean up any existing org mirror config."""
|
|
try:
|
|
org = model.organization.get_organization(orgname)
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
if config:
|
|
config.delete_instance()
|
|
except Exception as e:
|
|
logger.exception("Failed to cleanup org mirror config for org '%s': %s", orgname, e)
|
|
raise
|
|
|
|
|
|
class TestCreateOrgMirrorConfig:
|
|
"""Tests for POST /v1/organization/<orgname>/mirror endpoint."""
|
|
|
|
def test_create_org_mirror_config_success(self, app):
|
|
"""
|
|
Test successful creation of organization mirror configuration.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Verify the config was created
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config is not None
|
|
assert config.external_registry_url == "https://harbor.example.com"
|
|
assert config.external_namespace == "my-project"
|
|
assert config.sync_interval == 3600
|
|
assert config.is_enabled is True
|
|
|
|
# Clean up
|
|
config.delete_instance()
|
|
|
|
def test_create_org_mirror_config_with_optional_fields(self, app):
|
|
"""
|
|
Test creating org mirror config with all optional fields.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "quay",
|
|
"external_registry_url": "https://quay.io",
|
|
"external_namespace": "some-org",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "public",
|
|
"sync_interval": 7200,
|
|
"sync_start_date": "2025-06-15T12:00:00Z",
|
|
"is_enabled": False,
|
|
"external_registry_username": "myuser",
|
|
"external_registry_password": "mypassword",
|
|
"external_registry_config": {
|
|
"verify_tls": True,
|
|
"proxy": {
|
|
"https_proxy": "https://proxy.example.com",
|
|
},
|
|
},
|
|
"repository_filters": ["ubuntu*", "nginx"],
|
|
"skopeo_timeout": 600,
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config is not None
|
|
assert config.is_enabled is False
|
|
assert config.repository_filters == ["ubuntu*", "nginx"]
|
|
assert config.skopeo_timeout == 600
|
|
assert config.visibility.name == "public"
|
|
|
|
# Clean up
|
|
config.delete_instance()
|
|
|
|
def test_create_org_mirror_config_already_exists(self, app):
|
|
"""
|
|
Test that creating a config when one already exists returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create initial config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to create another config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "quay",
|
|
"external_registry_url": "https://quay.io",
|
|
"external_namespace": "other-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "public",
|
|
"sync_interval": 7200,
|
|
"sync_start_date": "2025-02-01T00:00:00Z",
|
|
}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400
|
|
)
|
|
assert "already exists" in resp.json.get("error_message", "")
|
|
|
|
# Clean up
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
if config:
|
|
config.delete_instance()
|
|
|
|
def test_create_org_mirror_config_invalid_robot(self, app):
|
|
"""
|
|
Test that creating config with invalid robot returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+nonexistent",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400
|
|
)
|
|
assert "Invalid robot" in resp.json.get("error_message", "")
|
|
|
|
def test_create_org_mirror_config_wrong_robot_namespace(self, app):
|
|
"""
|
|
Test that creating config with robot from different org returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "devtable+dtrobot", # Robot from different namespace
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400
|
|
)
|
|
assert "belong to the organization" in resp.json.get("error_message", "")
|
|
|
|
def test_create_org_mirror_config_invalid_registry_type(self, app):
|
|
"""
|
|
Test that creating config with invalid registry type returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "invalid_registry",
|
|
"external_registry_url": "https://example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
# Schema validation should catch this first
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400)
|
|
|
|
def test_create_org_mirror_config_invalid_visibility(self, app):
|
|
"""
|
|
Test that creating config with invalid visibility returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "invalid",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
# Schema validation should catch this first
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400)
|
|
|
|
def test_create_org_mirror_config_sync_interval_too_small(self, app):
|
|
"""
|
|
Test that creating config with sync_interval < 60 returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 30, # Too small
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
# Schema validation should catch this first
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400)
|
|
|
|
def test_create_org_mirror_config_invalid_date_format(self, app):
|
|
"""
|
|
Test that creating config with invalid date format returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "January 1, 2025", # Invalid format
|
|
}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400
|
|
)
|
|
assert "sync_start_date" in resp.json.get("error_message", "")
|
|
|
|
def test_create_org_mirror_config_org_not_found(self, app):
|
|
"""
|
|
Test that creating config for non-existent org returns 404.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "nonexistentorg"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "nonexistentorg+robot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 404)
|
|
|
|
def test_create_org_mirror_config_unauthorized(self, app):
|
|
"""
|
|
Test that creating config without proper permissions returns 403.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("reader", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 403)
|
|
|
|
def test_create_org_mirror_config_skopeo_timeout_out_of_range(self, app):
|
|
"""
|
|
Test that creating config with skopeo_timeout out of range returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Test too small
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
"skopeo_timeout": 10, # Too small (min 30)
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400)
|
|
|
|
# Test too large
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
"skopeo_timeout": 5000, # Too large (max 3600)
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 400)
|
|
|
|
|
|
class TestDeleteOrgMirrorConfig:
|
|
"""Tests for DELETE /v1/organization/<orgname>/mirror endpoint."""
|
|
|
|
def test_delete_org_mirror_config_success(self, app):
|
|
"""
|
|
Test successful deletion of organization mirror configuration.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# First create a config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Verify it exists
|
|
org = model.organization.get_organization("buynlarge")
|
|
assert model.org_mirror.get_org_mirror_config(org) is not None
|
|
|
|
# Delete it
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "DELETE", params, None, 204)
|
|
|
|
# Verify it's gone
|
|
assert model.org_mirror.get_org_mirror_config(org) is None
|
|
|
|
def test_delete_org_mirror_config_not_found(self, app):
|
|
"""
|
|
Test that deleting a non-existent config returns 404.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "DELETE", params, None, 404)
|
|
|
|
def test_delete_org_mirror_config_org_not_found(self, app):
|
|
"""
|
|
Test that deleting config for non-existent org returns 404.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "nonexistentorg"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "DELETE", params, None, 404)
|
|
|
|
def test_delete_org_mirror_config_unauthorized(self, app):
|
|
"""
|
|
Test that deleting config without proper permissions returns 403.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create a config directly via data model to avoid identity persistence issues
|
|
org = model.organization.get_organization("buynlarge")
|
|
robot = model.user.lookup_robot("buynlarge+coolrobot")
|
|
from data.database import SourceRegistryType, Visibility
|
|
|
|
model.org_mirror.create_org_mirror_config(
|
|
organization=org,
|
|
internal_robot=robot,
|
|
external_registry_type=SourceRegistryType.HARBOR,
|
|
external_registry_url="https://harbor.example.com",
|
|
external_namespace="my-project",
|
|
visibility=Visibility.get(name="private"),
|
|
sync_interval=3600,
|
|
sync_start_date=datetime.now(),
|
|
is_enabled=True,
|
|
)
|
|
|
|
# Verify config was created
|
|
assert model.org_mirror.get_org_mirror_config(org) is not None
|
|
|
|
# Try to delete as non-admin user (reader has member role, not admin)
|
|
with client_with_identity("reader", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "DELETE", params, None, 403)
|
|
|
|
# Verify config still exists
|
|
assert model.org_mirror.get_org_mirror_config(org) is not None
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_delete_org_mirror_config_can_recreate_after_delete(self, app):
|
|
"""
|
|
Test that after deleting a config, a new one can be created.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create first config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "project1",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Delete it
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "DELETE", params, None, 204)
|
|
|
|
# Create a new config with different settings
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "quay",
|
|
"external_registry_url": "https://quay.io",
|
|
"external_namespace": "project2",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "public",
|
|
"sync_interval": 7200,
|
|
"sync_start_date": "2025-06-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Verify the new config has the updated settings
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config is not None
|
|
assert config.external_registry_url == "https://quay.io"
|
|
assert config.external_namespace == "project2"
|
|
assert config.sync_interval == 7200
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
|
|
class TestUpdateOrgMirrorConfig:
|
|
"""Tests for PUT /v1/organization/<orgname>/mirror endpoint."""
|
|
|
|
def test_update_org_mirror_config_success(self, app):
|
|
"""
|
|
Test successful update of organization mirror configuration.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# First create a config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Update the config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"sync_interval": 7200,
|
|
"external_namespace": "updated-project",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
# Verify the update
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config is not None
|
|
assert config.sync_interval == 7200
|
|
assert config.external_namespace == "updated-project"
|
|
# Original values should be unchanged
|
|
assert config.external_registry_url == "https://harbor.example.com"
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_is_enabled(self, app):
|
|
"""
|
|
Test updating is_enabled field.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Disable mirroring
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"is_enabled": False}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.is_enabled is False
|
|
|
|
# Re-enable mirroring
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"is_enabled": True}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.is_enabled is True
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_visibility(self, app):
|
|
"""
|
|
Test updating visibility field.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config with private visibility
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Update to public visibility
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"visibility": "public"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.visibility.name == "public"
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_not_found(self, app):
|
|
"""
|
|
Test that updating a non-existent config returns 404.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"sync_interval": 7200}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 404)
|
|
|
|
def test_update_org_mirror_config_org_not_found(self, app):
|
|
"""
|
|
Test that updating config for non-existent org returns 404.
|
|
"""
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "nonexistentorg"}
|
|
request_body = {"sync_interval": 7200}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 404)
|
|
|
|
def test_update_org_mirror_config_unauthorized(self, app):
|
|
"""
|
|
Test that updating config without proper permissions returns 403.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create a config directly via data model to avoid identity persistence issues
|
|
org = model.organization.get_organization("buynlarge")
|
|
robot = model.user.lookup_robot("buynlarge+coolrobot")
|
|
from data.database import SourceRegistryType, Visibility
|
|
|
|
model.org_mirror.create_org_mirror_config(
|
|
organization=org,
|
|
internal_robot=robot,
|
|
external_registry_type=SourceRegistryType.HARBOR,
|
|
external_registry_url="https://harbor.example.com",
|
|
external_namespace="my-project",
|
|
visibility=Visibility.get(name="private"),
|
|
sync_interval=3600,
|
|
sync_start_date=datetime.now(),
|
|
is_enabled=True,
|
|
)
|
|
|
|
# Try to update as non-admin (reader has member role, not admin)
|
|
with client_with_identity("reader", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"sync_interval": 7200}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 403)
|
|
|
|
# Verify config was not changed
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.sync_interval == 3600
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_invalid_robot(self, app):
|
|
"""
|
|
Test that updating with invalid robot returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to update with non-existent robot
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"robot_username": "buynlarge+nonexistent"}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400
|
|
)
|
|
assert "Invalid robot" in resp.json.get("error_message", "")
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_wrong_robot_namespace(self, app):
|
|
"""
|
|
Test that updating with robot from different org returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to update with robot from different org
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"robot_username": "devtable+dtrobot"}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400
|
|
)
|
|
assert "belong to the organization" in resp.json.get("error_message", "")
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_sync_interval_too_small(self, app):
|
|
"""
|
|
Test that updating with sync_interval < 60 returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to update with invalid sync_interval
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"sync_interval": 30}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400)
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_invalid_visibility(self, app):
|
|
"""
|
|
Test that updating with invalid visibility returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to update with invalid visibility
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"visibility": "invalid"}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400)
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_invalid_date_format(self, app):
|
|
"""
|
|
Test that updating with invalid date format returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Try to update with invalid date format
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"sync_start_date": "January 1, 2025"}
|
|
resp = conduct_api_call(
|
|
cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400
|
|
)
|
|
assert "sync_start_date" in resp.json.get("error_message", "")
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_skopeo_timeout_out_of_range(self, app):
|
|
"""
|
|
Test that updating with skopeo_timeout out of range returns 400.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Test too small
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"skopeo_timeout": 10}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400)
|
|
|
|
# Test too large
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {"skopeo_timeout": 5000}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 400)
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_multiple_fields(self, app):
|
|
"""
|
|
Test updating multiple fields at once.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Update multiple fields
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"is_enabled": False,
|
|
"external_registry_url": "https://updated-harbor.example.com",
|
|
"external_namespace": "updated-project",
|
|
"visibility": "public",
|
|
"sync_interval": 7200,
|
|
"sync_start_date": "2025-06-01T12:00:00Z",
|
|
"repository_filters": ["ubuntu*", "nginx"],
|
|
"skopeo_timeout": 600,
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
# Verify all updates
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.is_enabled is False
|
|
assert config.external_registry_url == "https://updated-harbor.example.com"
|
|
assert config.external_namespace == "updated-project"
|
|
assert config.visibility.name == "public"
|
|
assert config.sync_interval == 7200
|
|
assert config.repository_filters == ["ubuntu*", "nginx"]
|
|
assert config.skopeo_timeout == 600
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
def test_update_org_mirror_config_credentials(self, app):
|
|
"""
|
|
Test updating credentials.
|
|
"""
|
|
_cleanup_org_mirror_config("buynlarge")
|
|
|
|
# Create config without credentials
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_type": "harbor",
|
|
"external_registry_url": "https://harbor.example.com",
|
|
"external_namespace": "my-project",
|
|
"robot_username": "buynlarge+coolrobot",
|
|
"visibility": "private",
|
|
"sync_interval": 3600,
|
|
"sync_start_date": "2025-01-01T00:00:00Z",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "POST", params, request_body, 201)
|
|
|
|
# Update with credentials
|
|
with client_with_identity("devtable", app) as cl:
|
|
params = {"orgname": "buynlarge"}
|
|
request_body = {
|
|
"external_registry_username": "newuser",
|
|
"external_registry_password": "newpassword",
|
|
}
|
|
conduct_api_call(cl, org_mirror.OrgMirrorConfig, "PUT", params, request_body, 200)
|
|
|
|
# Verify credentials were updated
|
|
org = model.organization.get_organization("buynlarge")
|
|
config = model.org_mirror.get_org_mirror_config(org)
|
|
assert config.external_registry_username is not None
|
|
assert config.external_registry_password is not None
|
|
|
|
# Clean up
|
|
_cleanup_org_mirror_config("buynlarge")
|