diff --git a/functional_tests/features/gitweb.feature b/functional_tests/features/gitweb.feature new file mode 100644 index 000000000..7802c91c3 --- /dev/null +++ b/functional_tests/features/gitweb.feature @@ -0,0 +1,107 @@ +# +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +@apps @gitweb @backups @sso +Feature: gitweb Simple Git Hosting + Git web interface. + +Background: + Given I'm a logged in user + And the gitweb application is installed + +Scenario: Enable gitweb application + Given the gitweb application is disabled + When I enable the gitweb application + Then the gitweb site should be available + +Scenario: Create public repository + Given the gitweb application is enabled + And a public repository that doesn't exist + When I create the repository + Then the repository should be listed as a public + And the repository should be listed on gitweb + +Scenario: Create private repository + Given the gitweb application is enabled + And a private repository that doesn't exist + When I create the repository + Then the repository should be listed as a private + And the repository should be listed on gitweb + +Scenario: Delete repository + Given the gitweb application is enabled + And a repository + When I delete the repository + Then the repository should not be listed + +Scenario: Backup and restore gitweb + Given the gitweb application is enabled + And a repository + When I create a backup of the gitweb app data + And I delete the repository + And I restore the gitweb app data backup + Then the repository should be restored + And the gitweb site should be available + +Scenario: Disable gitweb application + Given the gitweb application is enabled + When I disable the gitweb application + Then the gitweb site should not be available + +Scenario: Public gitweb site shows only public repositories + Given the gitweb application is enabled + And both public and private repositories exist + When I log out + Then the public repository should be listed on gitweb + And the private repository should not be listed on gitweb + +Scenario: Gitweb is not public if there are only private repositories + Given the gitweb application is enabled + And at least one repository exists + And all repositories are private + When I log out + And I access gitweb application + Then I should be prompted for login + And gitweb app should not be visible on the front page + +Scenario: Edit repository metadata + Given the gitweb application is enabled + And a public repository that doesn't exist + And a repository metadata: + description: Test Description + owner: Test Owner + access: private + When I create the repository + And I set the metadata of the repository + Then the metadata of the repository should be as set + +Scenario: Access public repository with git client + Given the gitweb application is enabled + And a public repository + When using a git client + Then the repository should be publicly readable + And the repository should not be publicly writable + And the repository should be privately writable + +Scenario: Access private repository with git client + Given the gitweb application is enabled + And a private repository + When using a git client + Then the repository should not be publicly readable + And the repository should not be publicly writable + And the repository should be privately readable + And the repository should be privately writable diff --git a/functional_tests/step_definitions/application.py b/functional_tests/step_definitions/application.py index 41c1a81f9..b5cbc1457 100644 --- a/functional_tests/step_definitions/application.py +++ b/functional_tests/step_definitions/application.py @@ -456,3 +456,133 @@ def app_visible_on_front_page(browser, app_name): def app_not_visible_on_front_page(browser, app_name): shortcuts = application.find_on_front_page(browser, app_name) assert len(shortcuts) == 0 + + +@given('a public repository') +@given('a repository') +@given('at least one repository exists') +def gitweb_repo(browser): + application.gitweb_create_repo(browser, 'Test-repo', 'public', True) + + +@given('a private repository') +def gitweb_private_repo(browser): + application.gitweb_create_repo(browser, 'Test-repo', 'private', True) + + +@given('both public and private repositories exist') +def gitweb_public_and_private_repo(browser): + application.gitweb_create_repo(browser, 'Test-repo', 'public', True) + application.gitweb_create_repo(browser, 'Test-repo2', 'private', True) + + +@given(parsers.parse("a {access:w} repository that doesn't exist")) +def gitweb_nonexistent_repo(browser, access): + application.gitweb_delete_repo(browser, 'Test-repo', ignore_missing=True) + return dict(access=access) + + +@given('all repositories are private') +def gitweb_all_repositories_private(browser): + application.gitweb_set_all_repos_private(browser) + + +@given(parsers.parse('a repository metadata:\n{metadata}')) +def gitweb_repo_metadata(browser, metadata): + metadata_dict = {} + for item in metadata.split('\n'): + item = item.split(': ') + metadata_dict[item[0]] = item[1] + return metadata_dict + + +@when('I create the repository') +def gitweb_create_repo(browser, access): + application.gitweb_create_repo(browser, 'Test-repo', access) + + +@when('I delete the repository') +def gitweb_delete_repo(browser): + application.gitweb_delete_repo(browser, 'Test-repo') + + +@when('I set the metadata of the repository') +def gitweb_edit_repo_metadata(browser, gitweb_repo_metadata): + application.gitweb_edit_repo_metadata(browser, 'Test-repo', + gitweb_repo_metadata) + + +@when('using a git client') +def gitweb_using_git_client(): + pass + + +@then('the repository should be restored') +@then('the repository should be listed as a public') +def gitweb_repo_should_exists(browser): + assert application.gitweb_repo_exists(browser, 'Test-repo', + access='public') + + +@then('the repository should be listed as a private') +def gitweb_private_repo_should_exists(browser): + assert application.gitweb_repo_exists(browser, 'Test-repo', 'private') + + +@then('the repository should not be listed') +def gitweb_repo_should_not_exist(browser, gitweb_repo): + assert not application.gitweb_repo_exists(browser, gitweb_repo) + + +@then('the public repository should be listed on gitweb') +@then('the repository should be listed on gitweb') +def gitweb_repo_should_exist_on_gitweb(browser): + assert application.gitweb_site_repo_exists(browser, 'Test-repo') + + +@then('the private repository should not be listed on gitweb') +def gitweb_private_repo_should_exists_on_gitweb(browser): + assert not application.gitweb_site_repo_exists(browser, 'Test-repo2') + + +@then('the metadata of the repository should be as set') +def gitweb_repo_metadata_should_match(browser, gitweb_repo_metadata): + actual_metadata = application.gitweb_get_repo_metadata( + browser, 'Test-repo') + assert all(item in actual_metadata.items() + for item in gitweb_repo_metadata.items()) + + +@then('the repository should be publicly readable') +def gitweb_repo_publicly_readable(): + assert application.gitweb_repo_is_readable('Test-repo') + assert application.gitweb_repo_is_readable('Test-repo', + url_git_extension=True) + + +@then('the repository should not be publicly readable') +def gitweb_repo_not_publicly_readable(): + assert not application.gitweb_repo_is_readable('Test-repo') + assert not application.gitweb_repo_is_readable('Test-repo', + url_git_extension=True) + + +@then('the repository should not be publicly writable') +def gitweb_repo_not_publicly_writable(): + assert not application.gitweb_repo_is_writable('Test-repo') + assert not application.gitweb_repo_is_writable('Test-repo', + url_git_extension=True) + + +@then('the repository should be privately readable') +def gitweb_repo_privately_readable(): + assert application.gitweb_repo_is_readable('Test-repo', with_auth=True) + assert application.gitweb_repo_is_readable('Test-repo', with_auth=True, + url_git_extension=True) + + +@then('the repository should be privately writable') +def gitweb_repo_privately_writable(): + assert application.gitweb_repo_is_writable('Test-repo', with_auth=True) + assert application.gitweb_repo_is_writable('Test-repo', with_auth=True, + url_git_extension=True) diff --git a/functional_tests/support/application.py b/functional_tests/support/application.py index 76d9714bf..fca0f0ef0 100644 --- a/functional_tests/support/application.py +++ b/functional_tests/support/application.py @@ -15,6 +15,11 @@ # along with this program. If not, see . # +import contextlib +import os +import shutil +import subprocess +import tempfile from time import sleep import requests @@ -319,6 +324,175 @@ def ejabberd_has_contact(browser): return eventually(site.jsxc_has_contact, [browser]) +def gitweb_create_repo(browser, repo, access=None, ok_if_exists=False): + """Create repository.""" + if not gitweb_repo_exists(browser, repo, access): + gitweb_delete_repo(browser, repo, ignore_missing=True) + browser.find_link_by_href('/plinth/apps/gitweb/create/').first.click() + browser.find_by_id('id_gitweb-name').fill(repo) + if access == 'private': + browser.find_by_id('id_gitweb-is_private').check() + elif access == 'public': + browser.find_by_id('id_gitweb-is_private').uncheck() + submit(browser) + elif not ok_if_exists: + assert False, 'Repo already exists.' + + +def gitweb_delete_repo(browser, repo, ignore_missing=False): + """Delete repository.""" + interface.nav_to_module(browser, 'gitweb') + delete_link = browser.find_link_by_href( + '/plinth/apps/gitweb/{}/delete/'.format(repo)) + if delete_link or not ignore_missing: + delete_link.first.click() + submit(browser) + + +def gitweb_edit_repo_metadata(browser, repo, metadata): + """Set repository metadata.""" + interface.nav_to_module(browser, 'gitweb') + browser.find_link_by_href( + '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click() + if 'name' in metadata: + browser.find_by_id('id_gitweb-name').fill(metadata['name']) + if 'description' in metadata: + browser.find_by_id('id_gitweb-description').fill( + metadata['description']) + if 'owner' in metadata: + browser.find_by_id('id_gitweb-owner').fill(metadata['owner']) + if 'access' in metadata: + if metadata['access'] == 'private': + browser.find_by_id('id_gitweb-is_private').check() + else: + browser.find_by_id('id_gitweb-is_private').uncheck() + submit(browser) + + +def gitweb_get_repo_metadata(browser, repo): + """Get repository metadata.""" + interface.nav_to_module(browser, 'gitweb') + browser.find_link_by_href( + '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click() + metadata = {} + for item in ['name', 'description', 'owner']: + metadata[item] = browser.find_by_id('id_gitweb-' + item).value + if browser.find_by_id('id_gitweb-is_private').value: + metadata['access'] = 'private' + else: + metadata['access'] = 'public' + return metadata + + +def _gitweb_get_repo_url(repo, with_auth): + """"Get repository URL""" + scheme = 'http' + if default_url.startswith('https://'): + scheme = 'https' + url = default_url.split('://')[1] if '://' in default_url else default_url + password = 'gitweb_wrong_password' + if with_auth: + password = config['DEFAULT']['password'] + + return '{0}://{1}:{2}@{3}/gitweb/{4}'.format( + scheme, config['DEFAULT']['username'], password, url, repo) + + +@contextlib.contextmanager +def _gitweb_temp_directory(): + """Create temporary directory""" + name = tempfile.mkdtemp(prefix='plinth_test_gitweb_') + yield name + shutil.rmtree(name) + + +def _gitweb_git_command_is_successful(command, cwd): + """Check if a command runs successfully or gives authentication error""" + process = subprocess.run(command, capture_output=True, cwd=cwd) + if process.returncode != 0: + if 'Authentication failed' in process.stderr.decode(): + return False + print(process.stdout.decode()) + # raise exception + process.check_returncode() + return True + + +def gitweb_repo_exists(browser, repo, access=None): + """Check whether the repository exists.""" + interface.nav_to_module(browser, 'gitweb') + links_found = browser.find_link_by_href('/gitweb/{}.git'.format(repo)) + access_matches = True + if links_found and access: + parent = links_found.first.find_by_xpath('..').first + private_icon = parent.find_by_css('.repo-private-icon') + if access == 'private': + access_matches = True if private_icon else False + if access == 'public': + access_matches = True if not private_icon else False + return bool(links_found) and access_matches + + +def gitweb_repo_is_readable(repo, with_auth=False, url_git_extension=False): + """Check if a git repo is readable with git client.""" + url = _gitweb_get_repo_url(repo, with_auth) + if url_git_extension: + url = url + '.git' + git_command = ['git', 'clone', '-c', 'http.sslverify=false', url] + with _gitweb_temp_directory() as cwd: + return _gitweb_git_command_is_successful(git_command, cwd) + + +def gitweb_repo_is_writable(repo, with_auth=False, url_git_extension=False): + """Check if a git repo is writable with git client.""" + url = _gitweb_get_repo_url(repo, with_auth) + if url_git_extension: + url = url + '.git' + + with _gitweb_temp_directory() as cwd: + subprocess.run(['mkdir', 'test-project'], check=True, cwd=cwd) + cwd = os.path.join(cwd, 'test-project') + prepare_git_repo_commands = [ + 'git init -q', 'git config http.sslVerify false', + 'git commit -q --allow-empty --author "Tester <>" -m "test"' + ] + for command in prepare_git_repo_commands: + subprocess.run(command, shell=True, check=True, cwd=cwd) + git_push_command = ['git', 'push', '-qf', url, 'master'] + + return _gitweb_git_command_is_successful(git_push_command, cwd) + + +def gitweb_set_repo_access(browser, repo, access): + """Set repository as public or private.""" + interface.nav_to_module(browser, 'gitweb') + browser.find_link_by_href( + '/plinth/apps/gitweb/{}/edit/'.format(repo)).first.click() + if access == 'private': + browser.find_by_id('id_gitweb-is_private').check() + else: + browser.find_by_id('id_gitweb-is_private').uncheck() + submit(browser) + + +def gitweb_set_all_repos_private(browser): + """Set all repositories private""" + interface.nav_to_module(browser, 'gitweb') + public_repos = [] + for element in browser.find_by_css('#gitweb-repo-list .list-group-item'): + if not element.find_by_css('.repo-private-icon'): + repo = element.find_by_css('.repo-label').first.text + public_repos.append(repo) + for repo in public_repos: + gitweb_set_repo_access(browser, repo, 'private') + + +def gitweb_site_repo_exists(browser, repo): + """Check whether the repository exists on Gitweb site.""" + browser.visit('{}/gitweb'.format(default_url)) + return browser.find_by_css('a[href="/gitweb/{0}.git"]'.format(repo)) + + def ikiwiki_create_wiki_if_needed(browser): """Create wiki if it does not exist.""" interface.nav_to_module(browser, 'ikiwiki') diff --git a/plinth/modules/gitweb/templates/gitweb_configure.html b/plinth/modules/gitweb/templates/gitweb_configure.html index 84b615f33..72e0d041a 100644 --- a/plinth/modules/gitweb/templates/gitweb_configure.html +++ b/plinth/modules/gitweb/templates/gitweb_configure.html @@ -54,7 +54,7 @@ {% if not repos %}

{% trans 'No repositories available.' %}

{% else %} -
+
{% for repo in repos %}