Files
awx/awx/main/tests/functional/test_projects.py
Alan Rominger c43dfde45a Create test for using manual & file projects (#15754)
* Create test for using a manual project

* Chang default project factory to git, remove project files monkeypatch

* skip update of factory project

* Initial file scaffolding for feature

* Fill in galaxy and names

* Add README, describe project folders and dependencies
2025-01-20 17:06:15 -05:00

470 lines
14 KiB
Python

# -*- coding: utf-8 -*-
from unittest import mock # noqa
import pytest
from awx.api.versioning import reverse
from awx.main.models import Project, JobTemplate
from django.core.exceptions import ValidationError
#
# Project listing and visibility tests
#
@pytest.fixture
def team_project_list(organization_factory):
objects = organization_factory(
'org-test',
superusers=['admin'],
users=['team1:alice', 'team2:bob'],
teams=['team1', 'team2'],
projects=['pteam1', 'pteam2', 'pshared'],
roles=[
'team1.member_role:pteam1.admin_role',
'team2.member_role:pteam2.admin_role',
'team1.member_role:pshared.admin_role',
'team2.member_role:pshared.admin_role',
],
)
return objects
@pytest.mark.django_db
def test_get_project_path(project):
# Test combining projects root with project local path
with mock.patch('awx.main.models.projects.settings.PROJECTS_ROOT', '/var/lib/foo'):
assert project.get_project_path(check_if_exists=False) == f'/var/lib/foo/_{project.id}__test_proj'
@pytest.mark.django_db
def test_user_project_paged_list(get, organization_factory):
'Test project listing that spans multiple pages'
# 3 total projects, 1 per page, 3 pages
objects = organization_factory(
'org1',
projects=['project-%s' % i for i in range(3)],
users=['alice'],
roles=['project-%s.admin_role:alice' % i for i in range(3)],
)
# first page has first project and no previous page
pk = objects.users.alice.pk
url = reverse(
'api:user_projects_list',
kwargs={
'pk': pk,
},
)
results = get(url, objects.users.alice, QUERY_STRING='page_size=1').data
assert results['count'] == 3
assert len(results['results']) == 1
assert results['previous'] is None
assert results['next'] == ('/api/v2/users/%s/projects/?page=2&page_size=1' % pk)
# second page has one more, a previous and next page
results = get(url, objects.users.alice, QUERY_STRING='page=2&page_size=1').data
assert len(results['results']) == 1
assert results['previous'] == ('/api/v2/users/%s/projects/?page=1&page_size=1' % pk)
assert results['next'] == ('/api/v2/users/%s/projects/?page=3&page_size=1' % pk)
# third page has last project and a previous page
results = get(url, objects.users.alice, QUERY_STRING='page=3&page_size=1').data
assert len(results['results']) == 1
assert results['previous'] == ('/api/v2/users/%s/projects/?page=2&page_size=1' % pk)
assert results['next'] is None
@pytest.mark.django_db
def test_user_project_paged_list_with_unicode(get, organization_factory):
'Test project listing that contains unicode chars in the next/prev links'
# Create 2 projects that contain a "cloud" unicode character, make sure we
# can search it and properly generate next/previous page links
objects = organization_factory(
'org1',
projects=['project-☁-1', 'project-☁-2'],
users=['alice'],
roles=['project-☁-1.admin_role:alice', 'project-☁-2.admin_role:alice'],
)
pk = objects.users.alice.pk
url = reverse(
'api:user_projects_list',
kwargs={
'pk': pk,
},
)
# first on first page, next page link contains unicode char
results = get(url, objects.users.alice, QUERY_STRING='page_size=1&search=%E2%98%81').data
assert results['count'] == 2
assert len(results['results']) == 1
assert results['next'] == ('/api/v2/users/%s/projects/?page=2&page_size=1&search=%%E2%%98%%81' % pk) # noqa
# second project on second page, previous page link contains unicode char
results = get(url, objects.users.alice, QUERY_STRING='page=2&page_size=1&search=%E2%98%81').data
assert results['count'] == 2
assert len(results['results']) == 1
assert results['previous'] == ('/api/v2/users/%s/projects/?page=1&page_size=1&search=%%E2%%98%%81' % pk) # noqa
@pytest.mark.django_db
def test_user_project_list(get, organization_factory):
'List of projects a user has access to, filtered by projects you can also see'
objects = organization_factory(
'org1',
projects=['alice project', 'bob project', 'shared project'],
superusers=['admin'],
users=['alice', 'bob'],
roles=['alice project.admin_role:alice', 'bob project.admin_role:bob', 'shared project.admin_role:bob', 'shared project.admin_role:alice'],
)
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.superusers.admin.pk,
},
),
objects.superusers.admin,
).data['count']
== 3
)
# admins can see everyones projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.users.alice.pk,
},
),
objects.superusers.admin,
).data['count']
== 2
)
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.users.bob.pk,
},
),
objects.superusers.admin,
).data['count']
== 2
)
# users can see their own projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.users.alice.pk,
},
),
objects.users.alice,
).data['count']
== 2
)
# alice should only be able to see the shared project when looking at bobs projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.users.bob.pk,
},
),
objects.users.alice,
).data['count']
== 1
)
# alice should see all projects they can see when viewing an admin
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': objects.superusers.admin.pk,
},
),
objects.users.alice,
).data['count']
== 2
)
@pytest.mark.django_db
def test_team_project_list(get, team_project_list):
objects = team_project_list
team1, team2 = objects.teams.team1, objects.teams.team2
alice, bob, admin = objects.users.alice, objects.users.bob, objects.superusers.admin
# admins can see all projects on a team
assert (
get(
reverse(
'api:team_projects_list',
kwargs={
'pk': team1.pk,
},
),
admin,
).data['count']
== 2
)
assert (
get(
reverse(
'api:team_projects_list',
kwargs={
'pk': team2.pk,
},
),
admin,
).data['count']
== 2
)
# users can see all projects on teams they are a member of
assert (
get(
reverse(
'api:team_projects_list',
kwargs={
'pk': team1.pk,
},
),
alice,
).data['count']
== 2
)
# but if she does, then she should only see the shared project
team2.read_role.members.add(alice)
assert (
get(
reverse(
'api:team_projects_list',
kwargs={
'pk': team2.pk,
},
),
alice,
).data['count']
== 1
)
team2.read_role.members.remove(alice)
# admins can see all projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': admin.pk,
},
),
admin,
).data['count']
== 3
)
# admins can see everyones projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': alice.pk,
},
),
admin,
).data['count']
== 2
)
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': bob.pk,
},
),
admin,
).data['count']
== 2
)
# users can see their own projects
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': alice.pk,
},
),
alice,
).data['count']
== 2
)
# alice should see all projects they can see when viewing an admin
assert (
get(
reverse(
'api:user_projects_list',
kwargs={
'pk': admin.pk,
},
),
alice,
).data['count']
== 2
)
@pytest.mark.parametrize("u,expected_status_code", [('rando', 403), ('org_member', 403), ('org_admin', 201), ('admin', 201)])
@pytest.mark.django_db
def test_create_project(post, organization, org_admin, org_member, admin, rando, u, expected_status_code):
if u == 'rando':
u = rando
elif u == 'org_member':
u = org_member
elif u == 'org_admin':
u = org_admin
elif u == 'admin':
u = admin
result = post(
reverse('api:project_list'),
{
'name': 'Project',
'organization': organization.id,
},
u,
expect=expected_status_code,
)
if expected_status_code == 201:
assert Project.objects.filter(name='Project', organization=organization).exists()
elif expected_status_code == 403:
assert 'do not have permission' in str(result.data['detail'])
@pytest.mark.django_db
def test_project_credential_protection(post, put, project, organization, scm_credential, org_admin):
project.save()
project.admin_role.members.add(org_admin)
put(reverse('api:project_detail', kwargs={'pk': project.id}), {'name': 'should not change', 'credential': scm_credential.id}, org_admin, expect=403)
post(reverse('api:project_list'), {'name': 'should not create', 'organization': organization.id, 'credential': scm_credential.id}, org_admin, expect=403)
@pytest.mark.django_db
def test_cannot_schedule_manual_project(manual_project, admin_user, post):
response = post(
reverse(
'api:project_schedules_list',
kwargs={
'pk': manual_project.pk,
},
),
{"name": "foo", "description": "", "enabled": True, "rrule": "DTSTART:20160926T040000Z RRULE:FREQ=HOURLY;INTERVAL=1", "extra_data": {}},
admin_user,
expect=400,
)
assert 'Manual' in response.data['unified_job_template'][0]
@pytest.mark.django_db
def test_project_unique_together_with_org(organization):
proj1 = Project(name='foo', organization=organization)
proj1.save()
proj2 = Project(name='foo', organization=organization)
with pytest.raises(ValidationError):
proj2.validate_unique()
proj2 = Project(name='foo', organization=None)
proj2.validate_unique()
@pytest.mark.django_db
def test_project_delete(delete, organization, admin_user):
proj = Project(name='foo', organization=organization)
proj.save()
delete(
reverse(
'api:project_detail',
kwargs={
'pk': proj.id,
},
),
admin_user,
)
@pytest.mark.parametrize(
'order_by, expected_names',
[
('name', ['alice project', 'bob project', 'shared project']),
('-name', ['shared project', 'bob project', 'alice project']),
],
)
@pytest.mark.django_db
def test_project_list_ordering_by_name(get, order_by, expected_names, organization_factory):
'ensure sorted order of project list is maintained correctly when the requested order is invalid or not applicable'
objects = organization_factory(
'org1',
projects=['alice project', 'bob project', 'shared project'],
superusers=['admin'],
)
project_names = []
# TODO: ask for an order by here that doesn't apply
results = get(reverse('api:project_list'), objects.superusers.admin, QUERY_STRING='order_by=%s' % order_by).data['results']
for x in range(len(results)):
project_names.append(results[x]['name'])
assert project_names == expected_names
@pytest.mark.parametrize('order_by', ('name', '-name'))
@pytest.mark.django_db
def test_project_list_ordering_with_duplicate_names(get, order_by, organization_factory):
# why? because all the '1' mean that all the names are the same, you can't sort based on that,
# meaning you have to fall back on the default sort order, which in this case, is ID
'ensure sorted order of project list is maintained correctly when the project names the same'
objects = organization_factory(
'org1',
projects=['1', '1', '1', '1', '1'],
superusers=['admin'],
)
project_ids = {}
for x in range(3):
results = get(reverse('api:project_list'), objects.superusers.admin, QUERY_STRING='order_by=%s' % order_by).data['results']
project_ids[x] = [proj['id'] for proj in results]
assert project_ids[0] == project_ids[1] == project_ids[2]
assert project_ids[0] == sorted(project_ids[0])
@pytest.mark.django_db
def test_project_failed_update(post, project, admin, inventory):
"""Test to ensure failed projects with update on launch will create launch rather than error"""
jt = JobTemplate.objects.create(project=project, inventory=inventory)
# set project to update on launch and set status to failed
project.update_fields(scm_update_on_launch=True)
project.update()
project.project_updates.last().update_fields(status='failed')
response = post(reverse('api:job_template_launch', kwargs={'pk': jt.pk}), user=admin, expect=201)
assert response.status_code == 201
# set project to not update on launch and validate still 400's
project.update_fields(scm_update_on_launch=False)
response = post(reverse('api:job_template_launch', kwargs={'pk': jt.pk}), user=admin, expect=400)
assert response.status_code == 400