[FastAPI] Modularize homepage and add side panel

This puts one more toward completion of the homepage
overall; we'll need to still implement the authenticated
user dashboard after this.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-07-28 13:28:17 -07:00
parent 9e73936c4e
commit d9cdd5faef
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
10 changed files with 500 additions and 102 deletions

View file

@ -1,7 +1,10 @@
from http import HTTPStatus from http import HTTPStatus
from typing import List
import orjson
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import and_ from sqlalchemy import and_, orm
from aurweb import db from aurweb import db
from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider
@ -10,6 +13,7 @@ from aurweb.models.package_base import PackageBase
from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_dependency import PackageDependency
from aurweb.models.package_relation import PackageRelation from aurweb.models.package_relation import PackageRelation
from aurweb.models.relation_type import PROVIDES_ID, RelationType from aurweb.models.relation_type import PROVIDES_ID, RelationType
from aurweb.redis import redis_connection
from aurweb.templates import register_filter from aurweb.templates import register_filter
@ -111,3 +115,52 @@ def get_pkgbase(name: str) -> PackageBase:
raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND))
return pkgbase return pkgbase
@register_filter("out_of_date")
def out_of_date(packages: orm.Query) -> orm.Query:
return packages.filter(PackageBase.OutOfDateTS.isnot(None))
def updated_packages(limit: int = 0, cache_ttl: int = 600) -> List[Package]:
""" Return a list of valid Package objects ordered by their
ModifiedTS column in descending order from cache, after setting
the cache when no key yet exists.
:param limit: Optional record limit
:param cache_ttl: Cache expiration time (in seconds)
:return: A list of Packages
"""
redis = redis_connection()
packages = redis.get("package_updates")
if packages:
# If we already have a cache, deserialize it and return.
return orjson.loads(packages)
query = db.query(Package).join(PackageBase).filter(
PackageBase.PackagerUID.isnot(None)
).order_by(
PackageBase.ModifiedTS.desc()
)
if limit:
query = query.limit(limit)
packages = []
for pkg in query:
# For each Package returned by the query, append a dict
# containing Package columns we're interested in.
packages.append({
"Name": pkg.Name,
"Version": pkg.Version,
"PackageBase": {
"ModifiedTS": pkg.PackageBase.ModifiedTS
}
})
# Store the JSON serialization of the package_updates key into Redis.
redis.set("package_updates", orjson.dumps(packages))
redis.expire("package_updates", cache_ttl)
# Return the deserialized list of packages.
return packages

View file

@ -1,14 +1,21 @@
""" AURWeb's primary routing module. Define all routes via @app.app.{get,post} """ AURWeb's primary routing module. Define all routes via @app.app.{get,post}
decorators in some way; more complex routes should be defined in their decorators in some way; more complex routes should be defined in their
own modules and imported here. """ own modules and imported here. """
from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Request from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import and_, or_
import aurweb.config import aurweb.config
from aurweb import util from aurweb import db, util
from aurweb.cache import db_count_cache
from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID
from aurweb.models.package_base import PackageBase
from aurweb.models.user import User
from aurweb.packages.util import updated_packages
from aurweb.templates import make_context, render_template from aurweb.templates import make_context, render_template
router = APIRouter() router = APIRouter()
@ -60,6 +67,71 @@ async def index(request: Request):
context = make_context(request, "Home") context = make_context(request, "Home")
context['ssh_fingerprints'] = util.get_ssh_fingerprints() context['ssh_fingerprints'] = util.get_ssh_fingerprints()
bases = db.query(PackageBase)
redis = aurweb.redis.redis_connection()
stats_expire = 300 # Five minutes.
updates_expire = 600 # Ten minutes.
# Package statistics.
query = bases.filter(PackageBase.PackagerUID.isnot(None))
context["package_count"] = await db_count_cache(
redis, "package_count", query, expire=stats_expire)
query = bases.filter(
and_(PackageBase.MaintainerUID.is_(None),
PackageBase.PackagerUID.isnot(None))
)
context["orphan_count"] = await db_count_cache(
redis, "orphan_count", query, expire=stats_expire)
query = db.query(User)
context["user_count"] = await db_count_cache(
redis, "user_count", query, expire=stats_expire)
query = query.filter(
or_(User.AccountTypeID == TRUSTED_USER_ID,
User.AccountTypeID == TRUSTED_USER_AND_DEV_ID))
context["trusted_user_count"] = await db_count_cache(
redis, "trusted_user_count", query, expire=stats_expire)
# Current timestamp.
now = int(datetime.utcnow().timestamp())
seven_days = 86400 * 7 # Seven days worth of seconds.
seven_days_ago = now - seven_days
one_hour = 3600
updated = bases.filter(
and_(PackageBase.ModifiedTS - PackageBase.SubmittedTS >= one_hour,
PackageBase.PackagerUID.isnot(None))
)
query = bases.filter(
and_(PackageBase.SubmittedTS >= seven_days_ago,
PackageBase.PackagerUID.isnot(None))
)
context["seven_days_old_added"] = await db_count_cache(
redis, "seven_days_old_added", query, expire=stats_expire)
query = updated.filter(PackageBase.ModifiedTS >= seven_days_ago)
context["seven_days_old_updated"] = await db_count_cache(
redis, "seven_days_old_updated", query, expire=stats_expire)
year = seven_days * 52 # Fifty two weeks worth: one year.
year_ago = now - year
query = updated.filter(PackageBase.ModifiedTS >= year_ago)
context["year_old_updated"] = await db_count_cache(
redis, "year_old_updated", query, expire=stats_expire)
query = bases.filter(
PackageBase.ModifiedTS - PackageBase.SubmittedTS < 3600)
context["never_updated"] = await db_count_cache(
redis, "never_updated", query, expire=stats_expire)
# Get the 15 most recently updated packages.
context["package_updates"] = updated_packages(15, updates_expire)
return render_template(request, "index.html", context) return render_template(request, "index.html", context)

101
templates/home.html Normal file
View file

@ -0,0 +1,101 @@
<div id="intro" class="box">
<h2>AUR {% trans %}Home{% endtrans %}</h2>
<p>
{{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information."
| tr
| format('<a href="https://wiki.archlinux.org/title/AUR_User_Guidelines">', "</a>",
'<a href="https://wiki.archlinux.org/title/AUR_Trusted_User_Guidelines">', "</a>")
| safe
}}
{{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!"
| tr
| format("<strong>", "</strong>",
'<a href="https://wiki.archlinux.org/title/Arch_Packaging_Standards">',
"</a>")
| safe
}}
{% trans %}Remember to vote for your favourite packages!{% endtrans %}
{% trans %}Some packages may be provided as binaries in [community].{% endtrans %}
</p>
<p class="important">
{% trans %}DISCLAIMER{% endtrans %}:
{% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %}
</p>
<p class="readmore"><a href="https://wiki.archlinux.org/title/AUR">{% trans %}Learn more...{% endtrans %}</a></p>
</div>
<div id="news">
<h3><a>{% trans %}Support{% endtrans %}</a><span class="arrow"></span></h3>
<h4>{% trans %}Package Requests{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:"
| tr
| format("<var>", "</var>")
| safe
}}
</p>
<ul>
<li><em>{% trans %}Orphan Request{% endtrans %}</em>: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}</li>
<li><em>{% trans %}Deletion Request{% endtrans %}</em>: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}</li>
<li><em>{% trans %}Merge Request{% endtrans %}</em>: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}</li>
</ul>
<p>
{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests."
| tr
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-requests">', "</a>")
| safe
}}
</p>
</div>
<h4>{% trans %}Submitting Packages{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details."
| tr
| format('<a href="https://wiki.archlinux.org/title/Arch_User_Repository#Submitting_packages">', "</a>")
| safe
}}
</p>
{% if ssh_fingerprints %}
<p>
{% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %}
<p>
<ul>
{% for keytype in ssh_fingerprints %}
<li><code>{{ keytype }}</code>: <code>{{ ssh_fingerprints[keytype] }}</code>
{% endfor %}
</ul>
{% endif %}
</div>
<h4>{% trans %}Discussion{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list."
| tr
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-general">', "</a>",
'<a href="https://mailman.archlinux.org/mailman/listinfo/aur-dev">', "</a>")
| safe
}}
<p>
</div>
<h4>{% trans %}Bug Reporting{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page."
| tr
| format('<a href="https://bugs.archlinux.org/index.php?project=2">', "</a>",
"<strong>", "</strong>")
| safe
}}
</p>
</div>
</div>
<!-- Bootstrap typeahead for the homepage. -->
<script type="text/javascript" src="/static/js/typeahead-home.js"></script>
<!-- Stub inline javascript with a nonce. Used for testing purposes. -->
<script type="text/javascript" nonce={{ request.user.nonce }}>
function NONEXISTENT() {}
NONEXISTENT();
</script>

View file

@ -1,106 +1,15 @@
{% extends 'partials/layout.html' %} {% extends 'partials/layout.html' %}
{% block pageContent %} {% block pageContent %}
<div id="intro" class="box"> <div id="content-left-wrapper">
<h2>AUR {% trans %}Home{% endtrans %}</h2> <div id="content-left">
<p> {% include 'home.html' %}
{{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information."
| tr
| format('<a href="https://wiki.archlinux.org/title/AUR_User_Guidelines">', "</a>",
'<a href="https://wiki.archlinux.org/title/AUR_Trusted_User_Guidelines">', "</a>")
| safe
}}
{{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!"
| tr
| format("<strong>", "</strong>",
'<a href="https://wiki.archlinux.org/title/Arch_Packaging_Standards">',
"</a>")
| safe
}}
{% trans %}Remember to vote for your favourite packages!{% endtrans %}
{% trans %}Some packages may be provided as binaries in [community].{% endtrans %}
<p class="important">
{% trans %}DISCLAIMER{% endtrans %}:
{% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %}
</p>
<p class="readmore"><a href="https://wiki.archlinux.org/title/AUR">{% trans %}Learn more...{% endtrans %}</a></p>
</p>
</div>
<div id="news">
<h3><a>{% trans %}Support{% endtrans %}</a><span class="arrow"></span></h3>
<h4>{% trans %}Package Requests{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:"
| tr
| format("<var>", "</var>")
| safe
}}
</p>
<ul>
<li><em>{% trans %}Orphan Request{% endtrans %}</em>: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}</li>
<li><em>{% trans %}Deletion Request{% endtrans %}</em>: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the package maintainer and file orphan request if necessary.{% endtrans %}</li>
<li><em>{% trans %}Merge Request{% endtrans %}</em>: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}</li>
</ul>
<p>
{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests."
| tr
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-requests">', "</a>")
| safe
}}
</p>
</div>
<h4>{% trans %}Submitting Packages{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details."
| tr
| format('<a href="https://wiki.archlinux.org/title/Arch_User_Repository#Submitting_packages">', "</a>")
| safe
}}
</p>
{% if ssh_fingerprints %}
<p>
{% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %}
<p>
<ul>
{% for keytype in ssh_fingerprints %}
<li><code>{{ keytype }}</code>: <code>{{ ssh_fingerprints[keytype] }}</code>
{% endfor %}
</ul>
{% endif %}
</div>
<h4>{% trans %}Discussion{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "General discussion regarding the Arch User Repository (AUR) and Trusted User structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list."
| tr
| format('<a href="https://mailman.archlinux.org/mailman/listinfo/aur-general">', "</a>",
'<a href="https://mailman.archlinux.org/mailman/listinfo/aur-dev">', "</a>")
| safe
}}
<p>
</div>
<h4>{% trans %}Bug Reporting{% endtrans %}</h4>
<div class="article-content">
<p>
{{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the package maintainer or leave a comment on the appropriate package page."
| tr
| format('<a href="https://bugs.archlinux.org/index.php?project=2">', "</a>",
"<strong>", "</strong>")
| safe
}}
</p>
</div> </div>
</div> </div>
<div id="content-right">
<!-- Bootstrap typeahead for the homepage. --> {% include 'partials/packages/widgets/search.html' %}
<script type="text/javascript" src="/static/js/typeahead-home.js"></script> {% include 'partials/packages/widgets/updates.html' %}
{% include 'partials/packages/widgets/statistics.html' %}
<!-- Stub inline javascript with a nonce. Used for testing purposes. --> </div>
<script type="text/javascript" nonce={{ request.user.nonce }}>
function NONEXISTENT() {}
NONEXISTENT();
</script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,14 @@
<div id="pkgsearch" class="widget">
<form id="pkgsearch-form" method="get" action="/packages/">
<fieldset>
<label for="pkgsearch-field">{{ "Package Search" | tr }}:</label>
<input type="hidden" name="O" value="0" />
<input id="pkgsearch-field"
type="text"
name="K"
size="30"
maxlength="35"
autocomplete="off" />
</fieldset>
</form>
</div>

View file

@ -0,0 +1,55 @@
<div id="pkg-stats" class="widget box">
<h3>{{ "Statistics" | tr }}</h3>
<table>
<tr>
<td class="stat-desc">{{ "Packages" | tr }}</td>
<td>{{ package_count }}</td>
</tr>
<tr>
<td class="stat-desc">{{ "Orphan Packages" | tr }}</td>
<td>{{ orphan_count }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Packages added in the past 7 days" | tr }}
</td>
<td>{{ seven_days_old_added }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Packages updated in the past 7 days" | tr }}
</td>
<td>{{ seven_days_old_updated }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Packages updated in the past year" | tr }}
</td>
<td>{{ year_old_updated }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Packages never updated" | tr }}
</td>
<td>{{ never_updated }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Registered Users" | tr }}
</td>
<td>{{ user_count }}</td>
</tr>
<tr>
<td class="stat-desc">
{{ "Trusted Users" | tr }}
</td>
<td>{{ trusted_user_count }}</td>
</tr>
</table>
</div>
{% if request.user.is_authenticated() %}
<!-- Include "My Statistics" -->
{% include 'partials/widgets/statistics.html' %}
{% endif %}

View file

@ -0,0 +1,35 @@
<div id="pkg-updates" class="widget box">
<h3>
{{ "Recent Updates" | tr }}
<span class="more">
(<a href="/packages/?SB=l&SO=d">{{ "more" | tr }}</a>)
</span>
</h3>
<a class="rss-icon latest" href="/rss/"
title="AUR Latest Packages RSS Feed">
<img src="/static/images/rss.svg" alt="RSS Feed" />
</a>
<a class="rss-icon" href="/rss/modified"
title="AUR Modified Packages RSS Feed">
<img src="/static/images/rss.svg" alt="RSS Feed" />
</a>
<table>
<tbody>
{% for pkg in package_updates %}
<tr>
<td class="pkg-name">
<a href="/packages/{{ pkg.Name }}">
{{ pkg.Name }} {{ pkg.Version }}
</a>
</td>
<td class="pkg-date">
{% set modified = pkg.PackageBase.ModifiedTS | dt | as_timezone(timezone) %}
{{ modified.strftime("%Y-%m-%d %H:%M") }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View file

@ -0,0 +1,27 @@
<div id="my-stats" class="widget box">
<h3>{{ "My Statistics" | tr }}</h3>
{% set bases = request.user.maintained_bases %}
<table>
<tbody>
<tr>
<td>
<a href="/packages/?SeB=m&K={{ request.user.Username }}">
{{ "Packages" | tr }}
</a>
</td>
<td>{{ bases.count() }}</td>
</tr>
{% set out_of_date_packages = bases | out_of_date %}
<tr>
<td>
<a href="/packages/?SeB=m&outdated=on&K={{ request.user.Username }}">
{{ "Out of Date" | tr }}
</a>
</td>
<td>{{ out_of_date_packages.count() }}</td>
</tr>
</tbody>
</table>
</div>

View file

@ -1,13 +1,82 @@
import re
from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from aurweb import db
from aurweb.asgi import app from aurweb.asgi import app
from aurweb.models.account_type import USER_ID
from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase
from aurweb.models.user import User
from aurweb.redis import redis_connection
from aurweb.testing import setup_test_db
from aurweb.testing.html import parse_root
client = TestClient(app) client = TestClient(app)
@pytest.fixture(autouse=True)
def setup():
yield setup_test_db(
User.__tablename__,
Package.__tablename__,
PackageBase.__tablename__
)
@pytest.fixture
def user():
yield db.create(User, Username="test", Email="test@example.org",
Passwd="testPassword", AccountTypeID=USER_ID)
@pytest.fixture
def redis():
redis = redis_connection()
def delete_keys():
# Cleanup keys if they exist.
for key in ("package_count", "orphan_count", "user_count",
"trusted_user_count", "seven_days_old_added",
"seven_days_old_updated", "year_old_updated",
"never_updated", "package_updates"):
if redis.get(key) is not None:
redis.delete(key)
delete_keys()
yield redis
delete_keys()
@pytest.fixture
def packages(user):
""" Yield a list of num_packages Package objects maintained by user. """
num_packages = 50 # Tunable
# For i..num_packages, create a package named pkg_{i}.
pkgs = []
now = int(datetime.utcnow().timestamp())
for i in range(num_packages):
pkgbase = db.create(PackageBase, Name=f"pkg_{i}",
Maintainer=user, Packager=user,
autocommit=False, SubmittedTS=now,
ModifiedTS=now)
pkg = db.create(Package, PackageBase=pkgbase,
Name=pkgbase.Name, autocommit=False)
pkgs.append(pkg)
now += 1
db.commit()
yield pkgs
def test_homepage(): def test_homepage():
with client as request: with client as request:
response = request.get("/") response = request.get("/")
@ -34,3 +103,49 @@ def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock):
response = request.get("/") response = request.get("/")
assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode()
def test_homepage_stats(redis, packages):
with client as request:
response = request.get("/")
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text)
expectations = [
("Packages", r'\d+'),
("Orphan Packages", r'\d+'),
("Packages added in the past 7 days", r'\d+'),
("Packages updated in the past 7 days", r'\d+'),
("Packages updated in the past year", r'\d+'),
("Packages never updated", r'\d+'),
("Registered Users", r'\d+'),
("Trusted Users", r'\d+')
]
stats = root.xpath('//div[@id="pkg-stats"]//tr')
for i, expected in enumerate(expectations):
expected_key, expected_regex = expected
key, value = stats[i].xpath('./td')
assert key.text.strip() == expected_key
assert re.match(expected_regex, value.text.strip())
def test_homepage_updates(redis, packages):
with client as request:
response = request.get("/")
assert response.status_code == int(HTTPStatus.OK)
# Run the request a second time to exercise the Redis path.
response = request.get("/")
assert response.status_code == int(HTTPStatus.OK)
root = parse_root(response.text)
# We expect to see the latest 15 packages, which happens to be
# pkg_49 .. pkg_34. So, create a list of expectations using a range
# starting at 49, stepping down to 49 - 15, -1 step at a time.
expectations = [f"pkg_{i}" for i in range(50 - 1, 50 - 1 - 15, -1)]
updates = root.xpath('//div[@id="pkg-updates"]/table/tbody/tr')
for i, expected in enumerate(expectations):
pkgname = updates[i].xpath('./td/a').pop(0)
assert pkgname.text.strip() == expected

View file

@ -9,6 +9,7 @@ from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase from aurweb.models.package_base import PackageBase
from aurweb.models.user import User from aurweb.models.user import User
from aurweb.packages import util from aurweb.packages import util
from aurweb.redis import kill_redis
from aurweb.testing import setup_test_db from aurweb.testing import setup_test_db
@ -33,7 +34,8 @@ def maintainer() -> User:
@pytest.fixture @pytest.fixture
def package(maintainer: User) -> Package: def package(maintainer: User) -> Package:
pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=maintainer) pkgbase = db.create(PackageBase, Name="test-pkg",
Packager=maintainer, Maintainer=maintainer)
yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) yield db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase)
@ -49,3 +51,18 @@ def test_package_link(client: TestClient, maintainer: User, package: Package):
Provides=package.Name) Provides=package.Name)
expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}"
assert util.package_link(package) == expected assert util.package_link(package) == expected
def test_updated_packages(maintainer: User, package: Package):
expected = {
"Name": package.Name,
"Version": package.Version,
"PackageBase": {
"ModifiedTS": package.PackageBase.ModifiedTS
}
}
kill_redis() # Kill it here to ensure we're on a fake instance.
assert util.updated_packages(1, 0) == [expected]
assert util.updated_packages(1, 600) == [expected]
kill_redis() # Kill it again, in case other tests use a real instance.