Ensure instance-state updates when collections are PUT or DELETE-ed #252

Merged
cmo merged 5 commits from fix-collection-management into master 2026-07-07 08:49:07 +00:00
3 changed files with 108 additions and 13 deletions

View file

@ -1,3 +1,29 @@
# 6.3.1 (2026-07-03)
## Changes
- Use canonical storage representation of records. This reduces "noise" in
audit-backends by minimizing the differences between record versions. This
also reduces the noise in change set in the `dtc`-subcommand `auto-curate`.
- `GET /tokens`-endpoint now returns full token information, including the
token representation. This allows to fully reproduce a configuration on
a different server via API-calls only.
- `gitaudit`-audit backends now use a fixed user-id. This prevents start up
errors in environments where no git-user id is configured.
# 6.3.0 (2026-06-29)
## Changes
- Automated annotation adding in write-record-endpoints has been removed.
The new query parameter `add_submission_tag` is added to write-record-endpoints.
If set to True, the server will add an automated annotation with the annotation
tags that are defined in the server-configuration (the default is False).
# 6.3.0 (2026-06-29)
## Changes

View file

@ -1,4 +1,5 @@
import logging
from typing import cast
from fastapi_pagination import add_pagination
@ -21,7 +22,7 @@ tag_groups = [
'incoming_write',
]
openapi_tags_template = [
openapi_tags_template: list[dict | str] = [
{
'name': 'Server management',
'description': 'General server operations',
@ -115,7 +116,7 @@ def manifest_configuration(
# because token-objects are all re-created below).
for collection_name in deleted_collection_names:
delete_endpoints_for_collection(instance_state, collection_name)
delete_collection(instance_state, collection_name)
delete_collection_from_instance_state(instance_state, collection_name)
# Create the internal representation objects for collections that have been
# added to the configuration.
@ -172,21 +173,28 @@ def delete_token(
instance_state.tokens.pop(token_name)
def delete_collection(
def delete_collection_from_instance_state(
instance_state: InstanceState,
collection_name: str,
):
instance_state.collections.pop(collection_name)
# TODO: remove further collection-related information from
# instance_state. Maybe all collection-specific information
# TODO: Maybe all collection-specific information
# should go into the instance_state.collection[x]-object!?
# That would allow to remove it easily.
# Remove objects that exist for every collection
instance_state.collections.pop(collection_name)
instance_state.curated_stores.pop(collection_name)
instance_state.validators.pop(collection_name)
instance_state.auth_sources.pop(collection_name)
# Remove optional objects
instance_state.audit_backends.pop(collection_name, None)
instance_state.incoming_stores.pop(collection_name, None)
def create_openapi_tags(
instance_state: InstanceState,
openapi_tags_template: list[dict | str],
openapi_tags_template_: list[dict | str],
) -> list[dict]:
# Collect tag name lists for all tag groups that we have defined.
tag_group_info = {
@ -199,8 +207,8 @@ def create_openapi_tags(
)
for tag_group in tag_groups
}
result = openapi_tags_template.copy()
result = openapi_tags_template_.copy()
for tag_group, tag_list in tag_group_info.items():
index = result.index(tag_group)
result[index : index + 1] = tag_list
return result
return cast('list[dict]', result)

View file

@ -12,12 +12,14 @@ from dump_things_service import (
HTTP_404_NOT_FOUND,
)
from dump_things_service.abstract_config import (
ForgejoAuthSpec,
GitAuditBackendConfig,
TokenCollectionConfig,
TokenModes,
hash_token_representation,
)
from dump_things_service.collection_endpoints import CollectionRequest
from dump_things_service.instance_state import get_instance_state
from dump_things_service.token_endpoints import (
AdminTokenRequest,
TokenRequest,
@ -82,15 +84,18 @@ def test_collection_adding(fastapi_client_simple):
assert response.status_code == HTTP_404_NOT_FOUND
assert not _name_in_openapi_paths(test_client, new_collection_name)
# Add a new collection
# Add a new collection to the configuration
response = test_client.post(
'/collections',
headers={'x-dumpthings-token': admin_token},
json=new_collection_request.model_dump(mode='json', by_alias=True),
)
assert response.status_code == HTTP_201_CREATED
# Check that the collection endpoints exist in the open API document
assert _name_in_openapi_paths(test_client, new_collection_name)
# Read back the new collection and compare it
response = test_client.get(
f'/collections/{new_collection_name}',
headers={'x-dumpthings-token': admin_token},
@ -124,13 +129,12 @@ def test_collection_adding(fastapi_client_simple):
'representation': new_token_request.representation,
}
# Add a new record to the collection to verify that the token works
new_record = {
'pid': 'http://example.com/admin-test-1',
'given_name': 'Admin Test 1',
'schema_type': 'abc:Person',
}
# Add a record to the collection
response = test_client.post(
f'/{new_collection_name}/record/Person',
headers={'x-dumpthings-token': new_token_representation},
@ -313,3 +317,60 @@ def test_admin_token_management(fastapi_client_simple):
)
assert response.status_code == HTTP_200_OK
assert new_admin_token_name not in response.json()
def test_instance_state_updates(fastapi_client_simple):
test_client, _, admin_token = fastapi_client_simple
instance_state = get_instance_state()
forgejo_auth_spec = ForgejoAuthSpec(
type='forgejo',
url='http://localhost',
organization='orga-1',
team='team-1',
label_type='user',
)
forgejo_audit_collection_name = 'test_forgejo_audit_collection'
forgejo_audit_collection_request = CollectionRequest(
name=forgejo_audit_collection_name,
default_token='test_default_token',
curated=PurePosixPath(f'{curated}/{forgejo_audit_collection_name}'),
schema=test_schema_location,
incoming=PurePosixPath(f'{incoming}/{forgejo_audit_collection_name}'),
auth_sources=[forgejo_auth_spec],
)
# Post a collection with forgejo audit-backend without an instance_id
response = test_client.post(
'/collections',
headers={'x-dumpthings-token': admin_token},
json=forgejo_audit_collection_request.model_dump(mode='json', by_alias=True),
)
assert response.status_code == HTTP_201_CREATED
forgejo_instance = instance_state.auth_sources[forgejo_audit_collection_name][0]
assert forgejo_instance.instance_id is None
# Put a collection with updated forgejo audit-backend, containing an instance_id
forgejo_auth_spec.instance_id = 'instance-id'
response = test_client.put(
'/collections',
headers={'x-dumpthings-token': admin_token},
json=forgejo_audit_collection_request.model_dump(mode='json', by_alias=True),
)
assert response.status_code == HTTP_201_CREATED
auth_sources = instance_state.auth_sources[forgejo_audit_collection_name]
assert len(auth_sources) == 1
new_forgejo_instance = auth_sources[0]
assert new_forgejo_instance is not forgejo_instance
assert new_forgejo_instance.instance_id == 'instance-id'
# Delete the collection again because we check for a known number of
# collections in other tests.
response = test_client.delete(
f'/collections/{forgejo_audit_collection_name}',
headers={'x-dumpthings-token': admin_token},
)
assert response.status_code == HTTP_200_OK