diff --git a/nomad/app/v1/routers/entries.py b/nomad/app/v1/routers/entries.py
index 22b46108c20a5930fbc8f83c9413839959a51af9..82dea922b18d39f2c8dd31aba6d8b4d90be21c61 100644
--- a/nomad/app/v1/routers/entries.py
+++ b/nomad/app/v1/routers/entries.py
@@ -202,6 +202,7 @@ class EntryRawDir(BaseModel):
     entry_id: str = Field(None)
     upload_id: str = Field(None)
     mainfile: str = Field(None)
+    mainfile_key: Optional[str] = Field(None)
     files: List[EntryRawDirFile] = Field(None)
 
 
@@ -467,6 +468,7 @@ def _create_entry_rawdir(entry_metadata: Dict[str, Any], uploads: _Uploads):
     entry_id = entry_metadata['entry_id']
     upload_id = entry_metadata['upload_id']
     mainfile = entry_metadata['mainfile']
+    mainfile_key = entry_metadata.get('mainfile_key')
 
     upload_files = uploads.get_upload_files(upload_id)
     mainfile_dir = os.path.dirname(mainfile)
@@ -475,7 +477,8 @@ def _create_entry_rawdir(entry_metadata: Dict[str, Any], uploads: _Uploads):
     for path_info in upload_files.raw_directory_list(mainfile_dir, files_only=True):
         files.append(EntryRawDirFile(path=path_info.path, size=path_info.size))
 
-    return EntryRawDir(entry_id=entry_id, upload_id=upload_id, mainfile=mainfile, files=files)
+    return EntryRawDir(
+        entry_id=entry_id, upload_id=upload_id, mainfile=mainfile, mainfile_key=mainfile_key, files=files)
 
 
 def _answer_entries_rawdir_request(
diff --git a/nomad/app/v1/routers/uploads.py b/nomad/app/v1/routers/uploads.py
index e149d105667c416d5bfec57866ebc4fea7154a6c..4858bce7ab13a188823f8ddfea512e1f395686e6 100644
--- a/nomad/app/v1/routers/uploads.py
+++ b/nomad/app/v1/routers/uploads.py
@@ -125,6 +125,7 @@ class EntryProcData(ProcData):
     entry_id: str = Field()
     entry_create_time: datetime = Field()
     mainfile: str = Field()
+    mainfile_key: Optional[str] = Field()
     upload_id: str = Field()
     parser_name: str = Field()
     entry_metadata: Optional[dict] = Field()
@@ -1040,15 +1041,19 @@ async def get_upload_entry_archive_mainfile(
         mainfile: str = Path(
             ...,
             description='The mainfile path within the upload\'s raw files.'),
+        mainfile_key: Optional[str] = FastApiQuery(
+            None,
+            description='The mainfile_key, for accessing child entries.'),
         user: User = Depends(create_user_dependency(required=False))):
     '''
     For the upload specified by `upload_id`, gets the full archive of a single entry that
     is identified by the given `mainfile`.
     '''
     _get_upload_with_read_access(upload_id, user, include_others=True)
-    return await answer_entry_archive_request(
-        dict(upload_id=upload_id, mainfile=mainfile),
-        required='*', user=user)
+    query = dict(upload_id=upload_id, mainfile=mainfile)
+    if mainfile_key:
+        query.update(mainfile_key=mainfile_key)
+    return await answer_entry_archive_request(query, required='*', user=user)
 
 
 @router.get(
diff --git a/tests/app/v1/routers/common.py b/tests/app/v1/routers/common.py
index 7705b2ce612ab2fbe2f68bcf4b8f5135d84348a1..9867113c8164a7adb3ad55db8c140d312439b3a4 100644
--- a/tests/app/v1/routers/common.py
+++ b/tests/app/v1/routers/common.py
@@ -590,6 +590,11 @@ def assert_metadata(response_json):
     for metadata in metadatas:
         if 'required' not in response_json:
             assert 'license' in metadata
+            if 'entry_id' in metadata:
+                if metadata['entry_id'].startswith('id_child_entries_child'):
+                    assert metadata['mainfile_key']
+                else:
+                    assert 'mainfile_key' not in metadata
 
         if 'main_author' in metadata:
             assert 'email' not in metadata['main_author']
diff --git a/tests/app/v1/routers/test_datasets.py b/tests/app/v1/routers/test_datasets.py
index 5841ebe6d001668bab2e5d4c25748445aaf19e53..3ed825344526369e908b8ac9be5b0cab1bc4cfdb 100644
--- a/tests/app/v1/routers/test_datasets.py
+++ b/tests/app/v1/routers/test_datasets.py
@@ -29,7 +29,7 @@ from nomad.utils.exampledata import ExampleData
 
 from tests.conftest import admin_user_id
 
-from .test_entries import data as example_entries  # pylint: disable=unused-import
+from tests.conftest import example_data  # pylint: disable=unused-import
 from .common import assert_response
 
 '''
@@ -220,7 +220,7 @@ def test_dataset(client, data, dataset_id, result, status_code):
     pytest.param('another test dataset', 'foreign', None, ['id_01', 'id_02'], 'test_user', 200, id='foreign-entries')
 ])
 def test_post_datasets(
-        client, data, example_entries, test_user, test_user_auth, other_test_user,
+        client, data, example_data, test_user, test_user_auth, other_test_user,
         other_test_user_auth, dataset_name, dataset_type, query, entries, user, status_code):
     dataset = {'dataset_name': dataset_name, 'dataset_type': dataset_type}
     if query is not None:
diff --git a/tests/app/v1/routers/test_entries.py b/tests/app/v1/routers/test_entries.py
index 1740daac8d97d1b5e5d991503be2a9453361ad56..e5a547e260a2c4eb72e1bff33a4e467f2393df7d 100644
--- a/tests/app/v1/routers/test_entries.py
+++ b/tests/app/v1/routers/test_entries.py
@@ -34,7 +34,7 @@ from .common import (
     perform_metadata_test, post_query_test_parameters, get_query_test_parameters,
     perform_owner_test, owner_test_parameters, pagination_test_parameters,
     aggregation_test_parameters)
-from tests.conftest import example_data as data  # pylint: disable=unused-import
+from tests.conftest import example_data  # pylint: disable=unused-import
 
 '''
 These are the tests for all API operations below ``entries``. The tests are organized
@@ -317,7 +317,7 @@ n_code_names = results.Simulation.program_name.a_elasticsearch[0].default_aggreg
 program_name = 'results.method.simulation.program_name'
 
 
-def test_entries_all_metrics(client, data):
+def test_entries_all_metrics(client, example_data):
     aggregations = {
         quantity: {
             'terms': {
@@ -352,7 +352,7 @@ def test_entries_all_metrics(client, data):
             {'terms': {'quantity': 'entry_id', 'include': '.*_0.*'}},
             -1, -1, 422, None, id='bad-filter')
     ])
-def test_entries_aggregations(client, data, test_user_auth, aggregation, total, size, status_code, user):
+def test_entries_aggregations(client, example_data, test_user_auth, aggregation, total, size, status_code, user):
     headers = {}
     if user == 'test_user':
         headers = test_user_auth
@@ -376,7 +376,7 @@ def test_entries_aggregations(client, data, test_user_auth, aggregation, total,
 @pytest.mark.parametrize(
     'query,agg_data,total,status_code',
     aggregation_exclude_from_search_test_parameters(resource='entries', total_per_entity=1, total=23))
-def test_entries_aggregations_exclude_from_search(client, data, query, agg_data, total, status_code):
+def test_entries_aggregations_exclude_from_search(client, example_data, query, agg_data, total, status_code):
     aggs, types, lengths = agg_data
     response_json = perform_entries_metadata_test(
         client, owner='visible',
@@ -404,7 +404,7 @@ def test_entries_aggregations_exclude_from_search(client, data, query, agg_data,
     pytest.param({'include': ['upload_id']}, 200, id='include-id')
 ])
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_entries_required(client, data, required, status_code, http_method):
+def test_entries_required(client, example_data, required, status_code, http_method):
     response_json = perform_entries_metadata_test(
         client, required=required, pagination={'page_size': 1}, status_code=status_code, http_method=http_method)
 
@@ -414,15 +414,17 @@ def test_entries_required(client, data, required, status_code, http_method):
     assert_required(response_json['data'][0], required, default_key='entry_id')
 
 
-@pytest.mark.parametrize('entry_id, required, status_code', [
-    pytest.param('id_01', {}, 200, id='id'),
-    pytest.param('doesnotexist', {}, 404, id='404'),
-    pytest.param('id_01', {'include': ['entry_id', 'upload_id']}, 200, id='include'),
-    pytest.param('id_01', {'exclude': ['upload_id']}, 200, id='exclude'),
-    pytest.param('id_01', {'exclude': ['entry_id', 'upload_id']}, 200, id='exclude-entry-id')
+@pytest.mark.parametrize('user, entry_id, required, status_code', [
+    pytest.param(None, 'id_01', {}, 200, id='id'),
+    pytest.param('test_user', 'id_child_entries_child1', {}, 200, id='id-child-entry'),
+    pytest.param(None, 'doesnotexist', {}, 404, id='404'),
+    pytest.param(None, 'id_01', {'include': ['entry_id', 'upload_id']}, 200, id='include'),
+    pytest.param(None, 'id_01', {'exclude': ['upload_id']}, 200, id='exclude'),
+    pytest.param(None, 'id_01', {'exclude': ['entry_id', 'upload_id']}, 200, id='exclude-entry-id')
 ])
-def test_entry_metadata(client, data, entry_id, required, status_code):
-    response = client.get('entries/%s?%s' % (entry_id, urlencode(required, doseq=True)))
+def test_entry_metadata(client, example_data, test_auth_dict, user, entry_id, required, status_code):
+    user_auth, _ = test_auth_dict[user]
+    response = client.get('entries/%s?%s' % (entry_id, urlencode(required, doseq=True)), headers=user_auth)
     response_json = assert_metadata_response(response, status_code=status_code)
 
     if response_json is None:
@@ -431,71 +433,83 @@ def test_entry_metadata(client, data, entry_id, required, status_code):
     assert_required(response_json['data'], required, default_key='entry_id')
 
 
-@pytest.mark.parametrize('query, files, total, files_per_entry, status_code', [
-    pytest.param({}, {}, 23, 5, 200, id='all'),
-    pytest.param({'entry_id': 'id_01'}, {}, 1, 5, 200, id='all'),
-    pytest.param({program_name: 'DOESNOTEXIST'}, {}, 0, 5, 200, id='empty')
+@pytest.mark.parametrize('user, owner, query, files, total, files_per_entry, status_code', [
+    pytest.param(None, None, {}, {}, 23, 5, 200, id='all'),
+    pytest.param(None, None, {'entry_id': 'id_01'}, {}, 1, 5, 200, id='one-entry'),
+    pytest.param('test_user', 'visible', {'upload_id': 'id_child_entries'}, {}, 3, 5, 200, id='child-entries'),
+    pytest.param(None, None, {program_name: 'DOESNOTEXIST'}, {}, 0, 5, 200, id='empty')
 ])
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_entries_rawdir(client, data, query, files, total, files_per_entry, status_code, http_method):
+def test_entries_rawdir(
+        client, example_data, test_auth_dict,
+        user, owner, query, files, total, files_per_entry, status_code, http_method):
+    user_auth, _ = test_auth_dict[user]
     perform_entries_rawdir_test(
-        client, status_code=status_code, query=query, files=files, total=total,
-        files_per_entry=files_per_entry, http_method=http_method)
-
-
-@pytest.mark.parametrize('query, files, total, files_per_entry, status_code', [
-    pytest.param({}, {}, 23, 5, 200, id='all'),
-    pytest.param({program_name: 'DOESNOTEXIST'}, {}, 0, 5, 200, id='empty'),
-    pytest.param({}, {'glob_pattern': '*.json'}, 23, 1, 200, id='glob'),
-    pytest.param({}, {'re_pattern': '[a-z]*\\.aux'}, 23, 4, 200, id='re'),
-    pytest.param({}, {'re_pattern': 'test_entry_02'}, 1, 5, 200, id='re-filter-entries'),
-    pytest.param({}, {'re_pattern': 'test_entry_02/.*\\.json'}, 1, 1, 200, id='re-filter-entries-and-files'),
-    pytest.param({}, {'glob_pattern': '*.json', 're_pattern': '.*\\.aux'}, 23, 4, 200, id='re-overwrites-glob'),
-    pytest.param({}, {'re_pattern': '**'}, -1, -1, 422, id='bad-re-pattern'),
-    pytest.param({}, {'compress': True}, 23, 5, 200, id='compress'),
-    pytest.param({}, {'include_files': ['1.aux']}, 23, 1, 200, id='file'),
-    pytest.param({}, {'include_files': ['1.aux', '2.aux']}, 23, 2, 200, id='files')
+        client, owner=owner, status_code=status_code, query=query, files=files, total=total,
+        files_per_entry=files_per_entry, http_method=http_method, headers=user_auth)
+
+
+@pytest.mark.parametrize('user, owner, query, files, total, files_per_entry, status_code', [
+    pytest.param(None, None, {}, {}, 23, 5, 200, id='all'),
+    pytest.param('test_user', 'visible', {'upload_id': 'id_child_entries'}, {}, (3, 1), 5, 200, id='child-entries'),
+    pytest.param(None, None, {program_name: 'DOESNOTEXIST'}, {}, 0, 5, 200, id='empty'),
+    pytest.param(None, None, {}, {'glob_pattern': '*.json'}, 23, 1, 200, id='glob'),
+    pytest.param(None, None, {}, {'re_pattern': '[a-z]*\\.aux'}, 23, 4, 200, id='re'),
+    pytest.param(None, None, {}, {'re_pattern': 'test_entry_02'}, 1, 5, 200, id='re-filter-entries'),
+    pytest.param(None, None, {}, {'re_pattern': 'test_entry_02/.*\\.json'}, 1, 1, 200, id='re-filter-entries-and-files'),
+    pytest.param(None, None, {}, {'glob_pattern': '*.json', 're_pattern': '.*\\.aux'}, 23, 4, 200, id='re-overwrites-glob'),
+    pytest.param(None, None, {}, {'re_pattern': '**'}, -1, -1, 422, id='bad-re-pattern'),
+    pytest.param(None, None, {}, {'compress': True}, 23, 5, 200, id='compress'),
+    pytest.param(None, None, {}, {'include_files': ['1.aux']}, 23, 1, 200, id='file'),
+    pytest.param(None, None, {}, {'include_files': ['1.aux', '2.aux']}, 23, 2, 200, id='files')
 ])
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_entries_raw(client, data, query, files, total, files_per_entry, status_code, http_method):
+def test_entries_raw(
+        client, example_data, test_auth_dict,
+        user, owner, query, files, total, files_per_entry, status_code, http_method):
+    user_auth, _ = test_auth_dict[user]
     perform_entries_raw_test(
-        client, status_code=status_code, query=query, files=files, total=total,
-        files_per_entry=files_per_entry, http_method=http_method)
+        client, headers=user_auth, owner=owner, status_code=status_code, query=query,
+        files=files, total=total, files_per_entry=files_per_entry, http_method=http_method)
 
 
 @pytest.mark.parametrize('http_method', ['post', 'get'])
 @pytest.mark.parametrize('test_method', [
     pytest.param(perform_entries_raw_test, id='raw'),
     pytest.param(perform_entries_archive_download_test, id='archive-download')])
-def test_entries_download_max(monkeypatch, client, data, test_method, http_method):
+def test_entries_download_max(monkeypatch, client, example_data, test_method, http_method):
     monkeypatch.setattr('nomad.config.max_entry_download', 20)
 
     test_method(client, status_code=400, http_method=http_method)
 
 
-@pytest.mark.parametrize('entry_id, files_per_entry, status_code', [
-    pytest.param('id_01', 5, 200, id='id'),
-    pytest.param('id_embargo', -1, 404, id='404'),
-    pytest.param('doesnotexist', -1, 404, id='404')])
-def test_entry_rawdir(client, data, entry_id, files_per_entry, status_code):
-    response = client.get('entries/%s/rawdir' % entry_id)
+@pytest.mark.parametrize('user, entry_id, files_per_entry, status_code', [
+    pytest.param(None, 'id_01', 5, 200, id='id'),
+    pytest.param('test_user', 'id_child_entries_child1', 5, 200, id='child-entries'),
+    pytest.param(None, 'id_embargo', -1, 404, id='embargoed'),
+    pytest.param(None, 'doesnotexist', -1, 404, id='bad-entry_id')])
+def test_entry_rawdir(client, example_data, test_auth_dict, user, entry_id, files_per_entry, status_code):
+    user_auth, _ = test_auth_dict[user]
+    response = client.get('entries/%s/rawdir' % entry_id, headers=user_auth)
     assert_response(response, status_code)
     if status_code == 200:
         assert_entry_rawdir_response(response.json(), files_per_entry=files_per_entry)
 
 
-@pytest.mark.parametrize('entry_id, files, files_per_entry, status_code', [
-    pytest.param('id_01', {}, 5, 200, id='id'),
-    pytest.param('doesnotexist', {}, -1, 404, id='404'),
-    pytest.param('id_01', {'glob_pattern': '*.json'}, 1, 200, id='glob'),
-    pytest.param('id_01', {'re_pattern': '[a-z]*\\.aux'}, 4, 200, id='re'),
-    pytest.param('id_01', {'re_pattern': '**'}, -1, 422, id='bad-re-pattern'),
-    pytest.param('id_01', {'compress': True}, 5, 200, id='compress'),
-    pytest.param('id_01', {'include_files': ['1.aux']}, 1, 200, id='file'),
-    pytest.param('id_01', {'include_files': ['1.aux', '2.aux']}, 2, 200, id='files')
+@pytest.mark.parametrize('user, entry_id, files, files_per_entry, status_code', [
+    pytest.param(None, 'id_01', {}, 5, 200, id='id'),
+    pytest.param('test_user', 'id_child_entries_child1', {}, 5, 200, id='child-entry'),
+    pytest.param(None, 'doesnotexist', {}, -1, 404, id='404'),
+    pytest.param(None, 'id_01', {'glob_pattern': '*.json'}, 1, 200, id='glob'),
+    pytest.param(None, 'id_01', {'re_pattern': '[a-z]*\\.aux'}, 4, 200, id='re'),
+    pytest.param(None, 'id_01', {'re_pattern': '**'}, -1, 422, id='bad-re-pattern'),
+    pytest.param(None, 'id_01', {'compress': True}, 5, 200, id='compress'),
+    pytest.param(None, 'id_01', {'include_files': ['1.aux']}, 1, 200, id='file'),
+    pytest.param(None, 'id_01', {'include_files': ['1.aux', '2.aux']}, 2, 200, id='files')
 ])
-def test_entry_raw(client, data, entry_id, files, files_per_entry, status_code):
-    response = client.get('entries/%s/raw?%s' % (entry_id, urlencode(files, doseq=True)))
+def test_entry_raw(client, example_data, test_auth_dict, user, entry_id, files, files_per_entry, status_code):
+    user_auth, _ = test_auth_dict[user]
+    response = client.get('entries/%s/raw?%s' % (entry_id, urlencode(files, doseq=True)), headers=user_auth)
     assert_response(response, status_code)
     if status_code == 200:
         assert_raw_zip_file(
@@ -546,6 +560,7 @@ def example_data_with_compressed_files(elastic_module, raw_files_module, mongo_m
 
 @pytest.mark.parametrize('entry_id, path, params, status_code', [
     pytest.param('id_01', 'mainfile.json', {}, 200, id='id'),
+    pytest.param('id_child_entries_child1', 'mainfile_w_children.json', {'user': 'test_user'}, 200, id='child-entry'),
     pytest.param('doesnotexist', 'mainfile.json', {}, 404, id='404-entry'),
     pytest.param('id_01', 'doesnot.exist', {}, 404, id='404-file'),
     pytest.param('id_01', 'mainfile.json', {'offset': 10, 'length': 10}, 200, id='offset-length'),
@@ -556,32 +571,27 @@ def example_data_with_compressed_files(elastic_module, raw_files_module, mongo_m
     pytest.param('id_01', 'mainfile.json', {'decompress': True}, 200, id='decompress-json'),
     pytest.param('with_compr_published', 'mainfile.xz', {'decompress': True}, 200, id='decompress-xz-published'),
     pytest.param('with_compr_published', 'mainfile.gz', {'decompress': True}, 200, id='decompress-gz-published'),
-    pytest.param('with_compr_unpublished', 'mainfile.xz', {'decompress': True, 'user': 'test-user'}, 200, id='decompress-xz-unpublished'),
-    pytest.param('with_compr_unpublished', 'mainfile.gz', {'decompress': True, 'user': 'test-user'}, 200, id='decompress-gz-unpublished'),
+    pytest.param('with_compr_unpublished', 'mainfile.xz', {'decompress': True, 'user': 'test_user'}, 200, id='decompress-xz-unpublished'),
+    pytest.param('with_compr_unpublished', 'mainfile.gz', {'decompress': True, 'user': 'test_user'}, 200, id='decompress-gz-unpublished'),
     pytest.param('id_unpublished', 'mainfile.json', {}, 404, id='404-unpublished'),
     pytest.param('id_embargo_1', 'mainfile.json', {}, 404, id='404-embargo-no-user'),
-    pytest.param('id_embargo_1', 'mainfile.json', {'user': 'other-test-user'}, 404, id='404-embargo-no-access'),
-    pytest.param('id_embargo_1', 'mainfile.json', {'user': 'test-user'}, 200, id='embargo-main_author'),
-    pytest.param('id_embargo_w_coauthor_1', 'mainfile.json', {'user': 'other-test-user'}, 200, id='embargo-coauthor'),
-    pytest.param('id_embargo_w_reviewer_1', 'mainfile.json', {'user': 'other-test-user'}, 200, id='embargo-reviewer')
+    pytest.param('id_embargo_1', 'mainfile.json', {'user': 'other_test_user'}, 404, id='404-embargo-no-access'),
+    pytest.param('id_embargo_1', 'mainfile.json', {'user': 'test_user'}, 200, id='embargo-main_author'),
+    pytest.param('id_embargo_w_coauthor_1', 'mainfile.json', {'user': 'other_test_user'}, 200, id='embargo-coauthor'),
+    pytest.param('id_embargo_w_reviewer_1', 'mainfile.json', {'user': 'other_test_user'}, 200, id='embargo-reviewer')
 ])
 def test_entry_raw_file(
-        client, data, example_data_with_compressed_files, example_mainfile_contents, test_user_auth, other_test_user_auth,
+        client, example_data, example_data_with_compressed_files, example_mainfile_contents, test_auth_dict,
         entry_id, path, params, status_code):
 
     user = params.get('user')
+    user_auth, _ = test_auth_dict[user]
     if user:
         del(params['user'])
-        if user == 'test-user':
-            headers = test_user_auth
-        elif user == 'other-test-user':
-            headers = other_test_user_auth
-    else:
-        headers = {}
 
     response = client.get(
         f'entries/{entry_id}/raw/{path}?{urlencode(params, doseq=True)}',
-        headers=headers)
+        headers=user_auth)
 
     assert_response(response, status_code)
     if status_code == 200:
@@ -594,16 +604,19 @@ def test_entry_raw_file(
             assert content == 'test content\n'
 
 
-@pytest.mark.parametrize('query, files, total, status_code', [
-    pytest.param({}, {}, 23, 200, id='all'),
-    pytest.param({program_name: 'DOESNOTEXIST'}, {}, -1, 200, id='empty'),
-    pytest.param({}, {'compress': True}, 23, 200, id='compress')
+@pytest.mark.parametrize('user, owner, query, files, total, status_code', [
+    pytest.param(None, None, {}, {}, 23, 200, id='all'),
+    pytest.param('test_user', 'visible', {'upload_id': 'id_child_entries'}, {}, 3, 200, id='child-entries'),
+    pytest.param(None, None, {program_name: 'DOESNOTEXIST'}, {}, -1, 200, id='empty'),
+    pytest.param(None, None, {}, {'compress': True}, 23, 200, id='compress')
 ])
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_entries_archive_download(client, data, query, files, total, status_code, http_method):
+def test_entries_archive_download(
+        client, example_data, test_auth_dict, user, owner, query, files, total, status_code, http_method):
+    user_auth, _ = test_auth_dict[user]
     perform_entries_archive_download_test(
-        client, status_code=status_code, query=query, files=files, total=total,
-        http_method=http_method)
+        client, headers=user_auth, owner=owner, status_code=status_code, query=query, http_method=http_method,
+        files=files, total=total)
 
 
 @pytest.mark.parametrize('required, status_code', [
@@ -613,28 +626,32 @@ def test_entries_archive_download(client, data, query, files, total, status_code
     pytest.param({'metadata': {'viewers[NOTANINT]': '*'}}, 422, id='bad-required-2'),
     pytest.param({'DOESNOTEXIST': '*'}, 422, id='bad-required-3')
 ])
-def test_entries_archive(client, data, required, status_code):
+def test_entries_archive(client, example_data, required, status_code):
     perform_entries_archive_test(
         client, status_code=status_code, required=required, http_method='post')
 
 
-@pytest.mark.parametrize('entry_id, status_code', [
-    pytest.param('id_01', 200, id='id'),
-    pytest.param('id_02', 404, id='404-not-visible'),
-    pytest.param('doesnotexist', 404, id='404-does-not-exist')])
-def test_entry_archive(client, data, entry_id, status_code):
-    response = client.get('entries/%s/archive' % entry_id)
+@pytest.mark.parametrize('user, entry_id, status_code', [
+    pytest.param(None, 'id_01', 200, id='id'),
+    pytest.param('test_user', 'id_child_entries_child1', 200, id='child-entry'),
+    pytest.param(None, 'id_02', 404, id='404-not-visible'),
+    pytest.param(None, 'doesnotexist', 404, id='404-does-not-exist')])
+def test_entry_archive(client, example_data, test_auth_dict, user, entry_id, status_code):
+    user_auth, _ = test_auth_dict[user]
+    response = client.get('entries/%s/archive' % entry_id, headers=user_auth)
     assert_response(response, status_code)
     if status_code == 200:
         assert_archive_response(response.json())
 
 
-@pytest.mark.parametrize('entry_id, status_code', [
-    pytest.param('id_01', 200, id='id'),
-    pytest.param('id_02', 404, id='404-not-visible'),
-    pytest.param('doesnotexist', 404, id='404-does-not-exist')])
-def test_entry_archive_download(client, data, entry_id, status_code):
-    response = client.get('entries/%s/archive/download' % entry_id)
+@pytest.mark.parametrize('user, entry_id, status_code', [
+    pytest.param(None, 'id_01', 200, id='id'),
+    pytest.param('test_user', 'id_child_entries_child1', 200, id='child-entry'),
+    pytest.param(None, 'id_02', 404, id='404-not-visible'),
+    pytest.param(None, 'doesnotexist', 404, id='404-does-not-exist')])
+def test_entry_archive_download(client, example_data, test_auth_dict, user, entry_id, status_code):
+    user_auth, _ = test_auth_dict[user]
+    response = client.get('entries/%s/archive/download' % entry_id, headers=user_auth)
     assert_response(response, status_code)
     if status_code == 200:
         archive = response.json()
@@ -642,20 +659,22 @@ def test_entry_archive_download(client, data, entry_id, status_code):
         assert 'run' in archive
 
 
-@pytest.mark.parametrize('entry_id, required, status_code', [
-    pytest.param('id_01', '*', 200, id='full'),
-    pytest.param('id_02', '*', 404, id='404'),
-    pytest.param('id_01', {'metadata': '*'}, 200, id='partial'),
-    pytest.param('id_01', {'run': {'system[NOTANINT]': '*'}}, 422, id='bad-required-1'),
-    pytest.param('id_01', {'metadata': {'viewers[NOTANINT]': '*'}}, 422, id='bad-required-2'),
-    pytest.param('id_01', {'DOESNOTEXIST': '*'}, 422, id='bad-required-3'),
-    pytest.param('id_01', {'resolve-inplace': 'NotBool', 'workflow': '*'}, 422, id='bad-required-4'),
-    pytest.param('id_01', {'resolve-inplace': True, 'metadata': 'include-resolved'}, 200, id='resolve-inplace')
+@pytest.mark.parametrize('user, entry_id, required, status_code', [
+    pytest.param(None, 'id_01', '*', 200, id='full'),
+    pytest.param('test_user', 'id_child_entries_child1', '*', 200, id='full-child-entry'),
+    pytest.param(None, 'id_02', '*', 404, id='404'),
+    pytest.param(None, 'id_01', {'metadata': '*'}, 200, id='partial'),
+    pytest.param(None, 'id_01', {'run': {'system[NOTANINT]': '*'}}, 422, id='bad-required-1'),
+    pytest.param(None, 'id_01', {'metadata': {'viewers[NOTANINT]': '*'}}, 422, id='bad-required-2'),
+    pytest.param(None, 'id_01', {'DOESNOTEXIST': '*'}, 422, id='bad-required-3'),
+    pytest.param(None, 'id_01', {'resolve-inplace': 'NotBool', 'workflow': '*'}, 422, id='bad-required-4'),
+    pytest.param(None, 'id_01', {'resolve-inplace': True, 'metadata': 'include-resolved'}, 200, id='resolve-inplace')
 ])
-def test_entry_archive_query(client, data, entry_id, required, status_code):
+def test_entry_archive_query(client, example_data, test_auth_dict, user, entry_id, required, status_code):
+    user_auth, _ = test_auth_dict[user]
     response = client.post('entries/%s/archive/query' % entry_id, json={
         'required': required
-    })
+    }, headers=user_auth)
     assert_response(response, status_code)
     if status_code == 200:
         assert_archive_response(response.json(), required=required)
@@ -679,7 +698,7 @@ n_elements = 'results.material.n_elements'
     pytest.param(perform_entries_rawdir_test, id='rawdir'),
     pytest.param(perform_entries_archive_test, id='archive'),
     pytest.param(perform_entries_archive_download_test, id='archive-download')])
-def test_entries_post_query(client, data, query, status_code, total, test_method):
+def test_entries_post_query(client, example_data, query, status_code, total, test_method):
     response_json = test_method(client, query=query, status_code=status_code, total=total, http_method='post')
 
     response = client.post('entries/query', json={'query': query})
@@ -708,7 +727,7 @@ def test_entries_post_query(client, data, query, status_code, total, test_method
     pytest.param(perform_entries_rawdir_test, id='rawdir'),
     pytest.param(perform_entries_archive_test, id='archive'),
     pytest.param(perform_entries_archive_download_test, id='archive-download')])
-def test_entries_get_query(client, data, query, status_code, total, test_method):
+def test_entries_get_query(client, example_data, query, status_code, total, test_method):
     response_json = test_method(
         client, query=query, status_code=status_code, total=total, http_method='get')
 
@@ -744,7 +763,7 @@ def test_entries_get_query(client, data, query, status_code, total, test_method)
     pytest.param(perform_entries_archive_test, id='archive'),
     pytest.param(perform_entries_archive_download_test, id='archive-download')])
 def test_entries_owner(
-        client, data, test_user_auth, other_test_user_auth, admin_user_auth,
+        client, example_data, test_user_auth, other_test_user_auth, admin_user_auth,
         owner, user, status_code, total_entries, total_mainfiles, total_materials,
         http_method, test_method):
 
@@ -764,7 +783,7 @@ def test_entries_owner(
     pytest.param(perform_entries_metadata_test, id='metadata'),
     pytest.param(perform_entries_rawdir_test, id='rawdir'),
     pytest.param(perform_entries_archive_test, id='archive')])
-def test_entries_pagination(client, data, pagination, response_pagination, status_code, http_method, test_method):
+def test_entries_pagination(client, example_data, pagination, response_pagination, status_code, http_method, test_method):
     response_json = test_method(
         client, pagination=pagination, status_code=status_code, http_method=http_method)
 
diff --git a/tests/app/v1/routers/test_materials.py b/tests/app/v1/routers/test_materials.py
index 0ba36e8d6395c4903d944574afe4f56d23f83cca..5013799dfca98bd871b2c5d916cc6c205282bb15 100644
--- a/tests/app/v1/routers/test_materials.py
+++ b/tests/app/v1/routers/test_materials.py
@@ -28,7 +28,7 @@ from .common import (
     perform_metadata_test, perform_owner_test, owner_test_parameters,
     post_query_test_parameters, get_query_test_parameters, pagination_test_parameters,
     aggregation_test_parameters)
-from tests.conftest import example_data as data  # pylint: disable=unused-import
+from tests.conftest import example_data  # pylint: disable=unused-import
 
 '''
 These are the tests for all API operations below ``entries``. The tests are organized
@@ -53,7 +53,7 @@ program_name = 'entries.results.method.simulation.program_name'
     'aggregation, total, size, status_code, user',
     aggregation_test_parameters(
         entity_id='material_id', material_prefix='', entry_prefix='entries.', total=6))
-def test_materials_aggregations(client, data, test_user_auth, aggregation, total, size, status_code, user):
+def test_materials_aggregations(client, example_data, test_user_auth, aggregation, total, size, status_code, user):
     headers = {}
     if user == 'test_user':
         headers = test_user_auth
@@ -82,7 +82,7 @@ def test_materials_aggregations(client, data, test_user_auth, aggregation, total
 @pytest.mark.parametrize(
     'query,agg_data,total,status_code',
     aggregation_exclude_from_search_test_parameters(resource='materials', total_per_entity=3, total=6))
-def test_materials_aggregations_exclude_from_search(client, data, query, agg_data, total, status_code):
+def test_materials_aggregations_exclude_from_search(client, example_data, query, agg_data, total, status_code):
     aggs, types, lengths = agg_data
     response_json = perform_materials_metadata_test(
         client, owner='visible',
@@ -110,7 +110,7 @@ def test_materials_aggregations_exclude_from_search(client, data, query, agg_dat
     pytest.param({'include': [program_name]}, 200, id='include-id')
 ])
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_materials_required(client, data, required, status_code, http_method):
+def test_materials_required(client, example_data, required, status_code, http_method):
     response_json = perform_materials_metadata_test(
         client, required=required, pagination={'page_size': 1}, status_code=status_code, http_method=http_method)
 
@@ -127,7 +127,7 @@ def test_materials_required(client, data, required, status_code, http_method):
     pytest.param('id_01', {'exclude': ['n_elements']}, 200, id='exclude'),
     pytest.param('id_01', {'exclude': ['material_id', 'n_elements']}, 200, id='exclude-id')
 ])
-def test_material_metadata(client, data, material_id, required, status_code):
+def test_material_metadata(client, example_data, material_id, required, status_code):
     response = client.get('materials/%s?%s' % (material_id, urlencode(required, doseq=True)))
     response_json = assert_metadata_response(response, status_code=status_code)
 
@@ -164,7 +164,7 @@ def test_material_metadata(client, data, material_id, required, status_code):
         pytest.param({'entry_id': 'id_01'}, 422, 0, id='not-material-quantity'),
         pytest.param({'entries.material_id': 'id_01'}, 422, 0, id='not-entry-quantity')
     ])
-def test_materials_post_query(client, data, query, status_code, total):
+def test_materials_post_query(client, example_data, query, status_code, total):
     response_json = perform_materials_metadata_test(
         client, query=query, status_code=status_code, total=total,
         http_method='post')
@@ -188,7 +188,7 @@ def test_materials_post_query(client, data, query, status_code, total):
 
 @pytest.mark.parametrize('query, status_code, total', get_query_test_parameters(
     'material_id', total=6, material_prefix='', entry_prefix='entries.'))
-def test_materials_get_query(client, data, query, status_code, total):
+def test_materials_get_query(client, example_data, query, status_code, total):
     assert 'entries.upload_create_time' in material_entry_type.quantities
 
     response_json = perform_materials_metadata_test(
@@ -222,7 +222,7 @@ def test_materials_get_query(client, data, query, status_code, total):
 @pytest.mark.parametrize('test_method', [
     pytest.param(perform_materials_metadata_test, id='metadata')])
 def test_materials_owner(
-        client, data, test_user_auth, other_test_user_auth, admin_user_auth,
+        client, example_data, test_user_auth, other_test_user_auth, admin_user_auth,
         owner, user, status_code, total_entries, total_mainfiles, total_materials,
         http_method, test_method):
 
@@ -235,7 +235,7 @@ def test_materials_owner(
     elements='elements', n_elements='n_elements', crystal_system='symmetry.crystal_system',
     total=6))
 @pytest.mark.parametrize('http_method', ['post', 'get'])
-def test_materials_pagination(client, data, pagination, response_pagination, status_code, http_method):
+def test_materials_pagination(client, example_data, pagination, response_pagination, status_code, http_method):
     response_json = perform_materials_metadata_test(
         client, pagination=pagination, status_code=status_code, http_method=http_method)
 
diff --git a/tests/app/v1/routers/test_uploads.py b/tests/app/v1/routers/test_uploads.py
index c208f57a4c2e95fed5bbcbfacd19157cb78b3047..64b9c2c0ea21d887541cbb69e33afeb155a91d31 100644
--- a/tests/app/v1/routers/test_uploads.py
+++ b/tests/app/v1/routers/test_uploads.py
@@ -476,6 +476,7 @@ def test_get_uploads(
 
 @pytest.mark.parametrize('user, upload_id, expected_status_code', [
     pytest.param('test_user', 'id_unpublished', 200, id='valid-upload_id'),
+    pytest.param('test_user', 'id_child_entries', 200, id='valid-upload_id-w-child-entries'),
     pytest.param('test_user', 'silly_value', 404, id='invalid-upload_id'),
     pytest.param(None, 'id_unpublished', 401, id='no-credentials'),
     pytest.param('invalid', 'id_unpublished', 401, id='invalid-credentials'),
@@ -501,6 +502,15 @@ def test_get_upload(
                 'total': 1, 'page': 1, 'page_after_value': None, 'next_page_after_value': None,
                 'page_url': Any, 'next_page_url': None, 'prev_page_url': None, 'first_page_url': Any}),
         id='no-args'),
+    pytest.param(
+        dict(
+            upload_id='id_child_entries',
+            expected_data_len=3,
+            expected_response={'processing_successful': 3, 'processing_failed': 0},
+            expected_pagination={
+                'total': 3, 'page': 1, 'page_after_value': None, 'next_page_after_value': None,
+                'page_url': Any, 'next_page_url': None, 'prev_page_url': None, 'first_page_url': Any}),
+        id='upload-w-child-entries'),
     pytest.param(
         dict(
             user=None,
@@ -643,6 +653,7 @@ def test_get_upload_entries(
 
 @pytest.mark.parametrize('upload_id, entry_id, user, expected_status_code', [
     pytest.param('id_embargo', 'id_embargo_1', 'test_user', 200, id='ok'),
+    pytest.param('id_child_entries', 'id_child_entries_child1', 'test_user', 200, id='child-entry'),
     pytest.param('id_embargo', 'id_embargo_1', None, 401, id='no-credentials'),
     pytest.param('id_embargo', 'id_embargo_1', 'invalid', 401, id='invalid-credentials'),
     pytest.param('id_embargo', 'id_embargo_1', 'other_test_user', 401, id='no-access'),
@@ -857,6 +868,10 @@ def test_get_upload_raw_path(
         'test_user', 'id_unpublished', 'test_content/id_unpublished_1/', {'include_entry_info': True},
         200, ['1.aux', '2.aux', '3.aux', '4.aux', 'mainfile.json'], None, {'total': 5},
         id='unpublished-dir-include_entry_info'),
+    pytest.param(
+        'test_user', 'id_child_entries', 'test_content', {'include_entry_info': True},
+        200, ['1.aux', '2.aux', '3.aux', '4.aux', 'mainfile_w_children.json'], None, {'total': 5},
+        id='dir-child-entries-include_entry_info'),
     pytest.param(
         'test_user', 'id_unpublished', '', {},
         200, ['test_content'], None, {'total': 1},
@@ -926,7 +941,8 @@ def test_get_upload_rawdir_path(
     pytest.param('id_published', 'test_content/doesnotexist.json', None, 404, id='bad-mainfile'),
     pytest.param('id_doesnotexist', 'test_content/subdir/test_entry_01/mainfile.json', None, 404, id='bad-upload-id'),
     pytest.param('id_unpublished', 'test_content/id_unpublished_1/mainfile.json', None, 401, id='unpublished'),
-    pytest.param('id_unpublished', 'test_content/id_unpublished_1/mainfile.json', 'test_user', 200, id='auth')
+    pytest.param('id_unpublished', 'test_content/id_unpublished_1/mainfile.json', 'test_user', 200, id='auth'),
+    pytest.param('id_child_entries', 'test_content/mainfile_w_children.json', 'test_user', 200, id='entry-w-child-entries')
 ])
 def test_get_upload_entry_archive_mainfile(
     client, example_data, test_auth_dict,
@@ -944,7 +960,8 @@ def test_get_upload_entry_archive_mainfile(
     pytest.param('id_published', 'doesnotexist', None, 404, id='bad-entry-id'),
     pytest.param('id_doesnotexist', 'id_01', None, 404, id='bad-upload-id'),
     pytest.param('id_unpublished', 'id_unpublished_1', None, 401, id='unpublished'),
-    pytest.param('id_unpublished', 'id_unpublished_1', 'test_user', 200, id='auth')
+    pytest.param('id_unpublished', 'id_unpublished_1', 'test_user', 200, id='auth'),
+    pytest.param('id_child_entries', 'id_child_entries_child1', 'test_user', 200, id='child-entry')
 ])
 def test_get_upload_entry_archive(
     client, example_data, test_auth_dict,
diff --git a/tests/app/v1/routers/test_users.py b/tests/app/v1/routers/test_users.py
index a08c2bfcbe7f8592b980dbacdb5c7c6c0f5fad24..092ea1379942982d40e97f93571419efa2058f47 100644
--- a/tests/app/v1/routers/test_users.py
+++ b/tests/app/v1/routers/test_users.py
@@ -75,9 +75,7 @@ def test_invite(client, test_user_auth, no_warn):
         [conf_test_users[conf_test_user_uuid(1)]],
         id='wrong-user-id')
 ])
-def test_users(
-        client, example_data, test_auth_dict,
-        args, expected_status_code, expected_content):
+def test_users(client, args, expected_status_code, expected_content):
     prefix = args.get('prefix', None)
     user_id = args.get('user_id', None)
 
@@ -116,9 +114,7 @@ def test_users(
         user_id=conf_test_user_uuid(1)), 200,
         conf_test_users[conf_test_user_uuid(1)],
         id='valid-user')])
-def test_users_id(
-        client, example_data, test_auth_dict,
-        args, expected_status_code, expected_content):
+def test_users_id(client, args, expected_status_code, expected_content):
     user_id = args['user_id']
     rv = client.get(f'users/{user_id}')
     assert rv.status_code == expected_status_code