feat: validate with user and org policy and tests
ci/woodpecker/push/woodpecker Pipeline failed Details

COMPONENTS

    FORGE
	API client to interact with target forge, for which
	shared-hosting is provided. Base class and Gitea implementation
	in check/forge.py

    PAYMENT VALIDATOR
	API client to interact with payments backed. It tells if a user
	is a paying customer or not. Defined in check/payment.py

    ALERT
	Alerting API that is called when a policy violation is
	discovered. Defined in check/alert.py

	An alert will include a message code and related data. All types
	of alerts should have unique message codes.

	WARNING
	    Using an undefined message code will result in raise
	    exceptions

    POLICY
	Features that are available to paying customers only. Defined in
	check/policy.py

USER POLICY
    - Only paying customers are allowed to create non-fork repositories
    - Non-paying customers are allowed to fork paying customers'
      repositories
    - Forks chain should contain at least one paying customer

ORG POLICY
    - At least 50% members of an organisation should be paying customers

TESTING STRATEGY
    Tests use dummy substitutes for `forge` API client, `payment
    validator` and the `alert` client.

    DATA
	- Test forge data is hard-coded and verified in
	  check/tests/test_forge.py.
	- Paying customer information is hard-coded in test_factory

    We run policies using this data and verify results
master
Aravinth Manivannan 1 year ago
parent 6f1765329a
commit 9cac98368b
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88

@ -0,0 +1,38 @@
define unimplemented
@echo "ERROR: Unimplemented!" && echo -1.
endef
define test_mod_check
. ./venv/bin/activate && coverage run -m pytest
. ./venv/bin/activate && coverage report -m
. ./venv/bin/activate && coverage html
endef
default: ## Run app
. ./venv/bin/activate && python -m check
coverage: ## Generate test coverage report
$(call test_mod_check)
doc: ## Generates documentation
$(call unimplemented)
docker: ## Build Docker image from source
$(call unimplemented)
env: ## Install all dependencies
@-virtualenv venv
. ./venv/bin/activate && pip install -r requirements.txt
# . ./venv/bin/activate && ./integration/ci.sh init
freeze: ## Freeze python dependencies
@. ./venv/bin/activate && pip freeze > requirements.txt
help: ## Prints help for targets with comments
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
lint: ## Run linter
@./venv/bin/black check/
test: ## Run tests
. venv/bin/activate && pytest

@ -0,0 +1,22 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from .factory import GiteaFactory
from .policy import UserPolicy, OrgPolicy
if __name__ == "__main__":
f = GiteaFactory()
for P in [UserPolicy, OrgPolicy]:
P(f=f).apply()

@ -0,0 +1,81 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from enum import Enum, unique
from .entity import User, Repo, Org
@unique
class MessageCode(Enum):
"""
Message code associated with alerts. All alert messages should contain
a message code
"""
USER_NO_PURCHASE_FORK_HISTORY = "The fork history doesn't contain a paying customer"
USER_NO_PURCHASE_NON_FORK_REPO = "Gratis customer has a non-fork repository"
ORG_MAJORITY_NO_PURCHASE = (
"Majority of the members of an organisation are non-paying customers"
)
def __str__(self) -> str:
return self.name
class Message:
"""
An alert message
"""
def __init__(
self,
code: MessageCode,
customer: User = None,
repo: Repo = None,
forks: [Repo] = None,
org: Org = None,
):
self.code = code
self.repo = repo
self.org = org
self.customer = customer
self.forks = forks
def __str__(self) -> str:
if self.code == MessageCode.USER_NO_PURCHASE_NON_FORK_REPO:
return f"{self.customer} is not a paying customer; non-fork repository: {self.repo}"
if self.code == MessageCode.USER_NO_PURCHASE_FORK_HISTORY:
return f"forks found without a paying customer at origin: {self.forks}"
if self.code == MessageCode.ORG_MAJORITY_NO_PURCHASE:
return f"org with more non-paying members than paying members: {self.org}"
raise Exception(f"Unknown message code {self.code}")
def __repr__(self) -> str:
return f"[{self.code}] {self}"
class Alert:
"""
Send Alert
"""
def alert(self, msg: Message):
"""
Ping admin: user|repository is over gratis quota
"""
print(msg)

@ -0,0 +1,61 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
class Repo:
"""
A Repository
"""
def __init__(self, owner: str, name: str, is_private: bool, is_fork: bool, parent):
self.owner = owner
self.name = name
self.is_private = is_private
self.is_fork = is_fork
self.parent = parent
def __repr__(self) -> str:
return self.__str__()
def __str__(self) -> str:
return f"{self.owner}/{self.name}"
class User:
"""
A User
"""
def __init__(self, username: str):
self.username = username
def __repr__(self) -> str:
return self.__str__()
def __str__(self) -> str:
return self.username
class Org:
"""
A Gitea organisation
"""
def __init__(self, username: str):
self.username = username
def __str__(self) -> str:
return self.username
def __repr__(self) -> str:
return self.__str__()

@ -0,0 +1,26 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
import os
gitea_sudo_token = os.environ.get("GITEA_SUDO_TOKEN")
gitea_url = os.environ.get("GITEA_URL")
if str.endswith(gitea_url, "/"):
gitea_url = gitea_url[0:-1]
sudo_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {gitea_sudo_token}",
}

@ -0,0 +1,29 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from .payment import Validator
from .alert import Alert
from .forge import Forge, Gitea
class Factory:
def __init__(self, forge: Forge, val: Validator, alert: Alert):
self.val = val
self.alert = alert
self.forge = forge
class GiteaFactory(Factory):
def __init__(self):
super().__init__(forge=Gitea(), val=Validator(), alert=Alert())

@ -0,0 +1,206 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
import requests
from .entity import User, Repo, Org
from .env import *
class Forge:
def get_all_users(self) -> [User]:
"""
Get all users on a forge instance
"""
raise NotImplementedError()
def get_all_orgs(self) -> [Org]:
"""
Get all organisations on a Gitea instance
"""
def get_repo(self, owner: str, name: str) -> Repo:
"""
Get repository from forge
"""
raise NotImplementedError()
def get_all_user_repos(self, username: str) -> [Repo]:
"""
Get all repositories that belong to a user on forge
"""
raise NotImplementedError()
def org_members(self, org_name: str) -> [User]:
"""
Get members of a Gitea organisation
"""
raise NotImplementedError()
def get_user(self, username: str) -> User:
"""
Get user from a forge instance
"""
def parent(self, repo: Repo) -> Repo:
"""
Get parent repository if repository is a fork
"""
if repo.is_fork:
return self.get_repo(
owner=repo.parent["owner"]["username"], name=repo.parent["name"]
)
raise Exception("Not a fork")
class Gitea(Forge):
def get_all_users(self) -> User:
"""
Get all users on a Gitea instance
"""
users = []
limit = 10
page = 1
while True:
resp = requests.get(
f"{gitea_url}/api/v1/admin/users?limit={limit}&page={page}",
timeout=5,
headers=sudo_headers,
)
if not resp.status_code != "200":
raise Exception(f"Unable to fetch users: {resp}")
data = resp.json()
if len(data) == 0:
break
page += 1
for u in data:
users.append(User(username=u["username"]))
return users
def get_user(self, username: str) -> User:
"""
Get user from Gitea
"""
headers = {"Authorization": f"Bearer {gitea_sudo_token}", "Sudo": username}
resp = requests.get(f"{gitea_url}/api/v1/user", timeout=5, headers=headers)
if not resp.status_code != "200":
raise Exception("User is not authenticated")
data = resp.json()
return User(username=data["username"])
def get_all_orgs(self) -> [Org]:
"""
Get all organisations on a Gitea instance
"""
orgs = []
limit = 10
page = 1
while True:
resp = requests.get(
f"{gitea_url}/api/v1/admin/orgs?limit={limit}&page={page}",
timeout=5,
headers=sudo_headers,
)
if not resp.status_code != "200":
raise Exception(f"Unable to fetch organisations: {resp}")
data = resp.json()
if len(data) == 0:
break
page += 1
for org in data:
orgs.append(Org(username=org["username"]))
# orgs.extend(data)
return orgs
def org_members(self, org_name: str) -> [User]:
"""
Get members of a Gitea organisation
"""
members = []
limit = 10
page = 1
while True:
resp = requests.get(
f"{gitea_url}/api/v1/orgs/{org_name}/members?limit={limit}&page={page}",
timeout=5,
headers=sudo_headers,
)
if not resp.status_code != "200":
raise Exception(f"Unable to fetch organisation {org_name}: {resp}")
data = resp.json()
if len(data) == 0:
break
for member in data:
members.append(User(username=member["username"]))
page += 1
return members
def get_all_user_repos(self, username: str) -> "Repos":
"""
Get all repositories that belong to a user on forge
"""
repos = []
limit = 10
page = 1
headers = {"Authorization": f"Bearer {gitea_sudo_token}", "Sudo": username}
while True:
resp = requests.get(
f"{gitea_url}/api/v1/user/repos?limit={limit}&page={page}",
timeout=5,
headers=headers,
)
if not resp.status_code != "200":
raise Exception(f"Unable to fetch repos: {resp}")
data = resp.json()
if len(data) == 0:
break
page += 1
for r in data:
repos.append(
Repo(
owner=r["owner"]["username"],
name=r["name"],
is_private=r["private"],
is_fork=r["fork"],
parent=r["parent"],
)
)
return repos
def get_repo(self, owner: str, name: str) -> Repo:
"""
Get repository from forge
"""
resp = requests.get(
f"{gitea_url}/api/v1/repos/{owner}/{name}",
timeout=5,
headers=sudo_headers,
)
if not resp.status_code != "200":
raise Exception(f"Unable to fetch repository {owner}/{name}: {resp}")
data = resp.json()
return Repo(
owner=data["owner"]["username"],
name=data["name"],
is_private=data["private"],
is_fork=data["fork"],
parent=data["parent"],
)

@ -0,0 +1,27 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from .entity import User
class Validator:
"""
Payment validator
"""
def is_paying(self, user: User) -> bool:
"""
Check if user is a paying customer
"""
return False

@ -0,0 +1,118 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from .entity import User
from .factory import Factory
from .alert import Message, MessageCode
class GroupPolicy:
"""
Payment policy to govern a group of actors
"""
def __init__(self, f: Factory):
self.f = f
self.validator = f.val
self.alert = f.alert
self.forge = f.forge
def apply(self):
"""
Apply policies to group
Account paying and non-paying users. Notify admin if non-paying members are
are exceeding their gratis quota
"""
raise NotImplementedError()
class UserPolicy(GroupPolicy):
"""
Payment policy to govern a group of actors, where an actor is a Gitea user
"""
def __init__(self, f: Factory):
super().__init__(f=f)
def apply(self):
"""
Account paying and non-paying users. Notify admin if non-paying members are
are exceeding their gratis quota
"""
users = self.forge.get_all_users()
for user in users:
if self.validator.is_paying(user=user):
continue
repos = self.forge.get_all_user_repos(user.username)
for repo in repos:
if not repo.is_fork:
self.alert.alert(
Message(
code=MessageCode.USER_NO_PURCHASE_NON_FORK_REPO,
customer=user,
repo=repo,
)
)
continue
forks = []
cur_repo = repo
forks.append(cur_repo)
while cur_repo.is_fork:
parent = self.forge.parent(repo=cur_repo) # cur_repo.parent()
parent_owner = User(username=parent.owner)
cur_repo = parent
forks.append(cur_repo)
if self.validator.is_paying(user=parent_owner):
break
if not self.validator.is_paying(
user=self.forge.get_user(cur_repo.owner)
):
self.alert.alert(
Message(
code=MessageCode.USER_NO_PURCHASE_FORK_HISTORY, forks=forks
)
)
class OrgPolicy(GroupPolicy):
"""
Payment policy to govern a group of actors, where an actor is a Gitea organisation
"""
def __init__(self, f: Factory):
super().__init__(f=f)
def apply(self):
"""
Account paying and non-paying users. Notify admin if non-paying members are
are exceeding their gratis quota
"""
orgs = self.forge.get_all_orgs()
for org in orgs:
members = self.forge.org_members(org_name=org.username) # org.members()
paying = 0
for member in members:
if self.validator.is_paying(user=member):
paying += 1
if not paying >= (len(members) / 2):
self.alert.alert(
Message(code=MessageCode.ORG_MAJORITY_NO_PURCHASE, org=org)
)

@ -0,0 +1,34 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from check.alert import Alert, Message
class DummyAlert(Alert):
def __init__(self):
super().__init__()
self.msg = []
def alert(self, msg: Message):
self.msg.append(msg)
def test_alert():
alert = DummyAlert()
msg = ["foo", "bar"]
for m in msg:
alert.alert(m)
for (i, m) in enumerate(msg):
assert alert.msg[i] == m

@ -0,0 +1,43 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from check.factory import Factory
from .test_payments import DummyValidator
from .test_forge import DummyForge
from .test_alert import DummyAlert
class DummyFactory(Factory):
def __init__(self):
forge = DummyForge()
val = DummyValidator()
paying_users = forge.users[0:3]
val.set_paying_users(paying_users)
super().__init__(forge=forge, val=val, alert=DummyAlert())
def test_dummy_factory():
f = DummyFactory()
paying = 0
non_paying = 0
for user in f.forge.users:
if f.val.is_paying(user=user):
paying+=1
else:
non_paying+=1
assert paying == non_paying == 3
for cus in f.val.paying_users:
assert "no-paying" not in cus

@ -0,0 +1,195 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from check.forge import Forge
from check.entity import User, Repo, Org
class DummyForge(Forge):
def __init__(self):
super().__init__()
self.users = [
User(username="paying_user1"),
User(username="paying_user2"),
User(username="paying_user3"),
User(username="no-paying_user1"),
User(username="no-paying_user2"),
User(username="no-paying_user3"),
]
self.orgs = [
Org(username="org1"),
Org(username="org2"),
Org(username="org3"),
]
self.org_members_matrix = {}
# 50:50::paying:non-paying
self.org_members_matrix[self.orgs[0].username] = [
self.users[0],
self.users[1],
self.users[3],
self.users[4],
]
# paying < non-paying
self.org_members_matrix[self.orgs[1].username] = [
self.users[0],
self.users[3],
self.users[4],
self.users[5],
]
# paying > non-paying
self.org_members_matrix[self.orgs[2].username] = [
self.users[0],
self.users[1],
self.users[2],
self.users[5],
]
self.repos = [
Repo(
owner=self.users[0].username,
name="repo1",
is_private=False,
is_fork=False,
parent=None,
),
Repo(
owner=self.users[1].username,
name="repo2",
is_private=False,
is_fork=False,
parent=None,
),
Repo(
owner=self.users[2].username,
name="repo3",
is_private=False,
is_fork=True,
parent=None,
),
Repo(
owner=self.users[3].username,
name="repo4",
is_private=False,
is_fork=False,
parent=None,
),
]
self.repos.append(
Repo(
owner=self.users[4].username,
name="repo5",
is_private=False,
is_fork=True,
parent={
"owner": {
"username": self.repos[2].owner,
},
"name": self.repos[2].name,
},
)
)
self.repos.append(
Repo(
owner=self.users[5].username,
name="repo6",
is_private=False,
is_fork=True,
parent={
"owner": {
"username": self.repos[3].owner,
},
"name": self.repos[3].name,
},
)
)
def get_all_users(self) -> [User]:
"""
Get all users on a forge instance
"""
return self.users
def get_all_orgs(self) -> [Org]:
"""
Get all organisations on a Gitea instance
"""
return self.orgs
def get_repo(self, owner: str, name: str) -> Repo:
"""
Get repository from forge
"""
for repo in self.repos:
if all([repo.owner == owner, repo.name == name]):
return repo
return None
def get_all_user_repos(self, username: str) -> [Repo]:
"""
Get all repositories that belong to a user on forge
"""
repos = []
for repo in self.repos:
if repo.owner == username:
repos.append(repo)
return repos
def org_members(self, org_name: str) -> [User]:
"""
Get members of a Gitea organisation
"""
return self.org_members_matrix[org_name]
def get_user(self, username: str) -> User:
"""
Get user from a forge instance
"""
for user in self.users:
if user.username == username:
return user
return None
def test_forge():
forge = DummyForge()
assert forge.get_user(forge.users[1].username) is forge.users[1]
assert forge.get_user(username="nouser") is None
assert (
forge.org_members(org_name=forge.orgs[0].username)
is forge.org_members_matrix[forge.orgs[0].username]
)
assert forge.get_all_user_repos(username=forge.users[1].username) == [
forge.repos[1]
]
assert (
forge.get_repo(owner=forge.repos[0].owner, name=forge.repos[0].name)
is forge.repos[0]
)
assert forge.get_repo(owner="nouser", name="norepo") is None
assert forge.get_repo(owner=forge.repos[0].owner, name="norepo") is None
assert forge.get_repo(owner="norepo", name=forge.repos[0].name) is None
assert forge.get_all_orgs() is forge.orgs
assert forge.get_all_users() is forge.users

@ -0,0 +1,39 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from check.payment import Validator
from check.entity import User
class DummyValidator(Validator):
def __init__(self):
super().__init__()
self.paying_users = {}
def set_paying_users(self, paying_users: [User]):
for user in paying_users:
self.paying_users[user.username] = user
def is_paying(self, user: User) -> bool:
return user.username in self.paying_users
def test_validator():
paying_users = [User(username="foo")]
gratis_user = User(username="bar")
v = DummyValidator()
v.set_paying_users(paying_users)
assert v.is_paying(gratis_user) is False
assert v.is_paying(paying_users[0]) is True

@ -0,0 +1,45 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# 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 <http://www.gnu.org/licenses/>.
from check.policy import UserPolicy, OrgPolicy
from check.alert import MessageCode
from .test_factory import DummyFactory
def test_user_policy():
f = DummyFactory()
UserPolicy(f=f).apply()
no_purchase_fork = f.alert.msg[1]
assert no_purchase_fork.code == MessageCode.USER_NO_PURCHASE_FORK_HISTORY
assert no_purchase_fork.forks == [
f.forge.get_repo(owner="no-paying_user3", name="repo6"),
f.forge.get_repo(owner="no-paying_user1", name="repo4"),
]
no_purchase_non_fork = f.alert.msg[0]
assert no_purchase_non_fork.code == MessageCode.USER_NO_PURCHASE_NON_FORK_REPO
assert no_purchase_non_fork.customer == f.forge.get_user(username="no-paying_user1")
def test_validator_with_for_data():
f = DummyFactory()
assert f.val.is_paying(f.forge.users[3]) is False
def test_org_policy():
f = DummyFactory()
OrgPolicy(f=f).apply()
no_purchase_fork = f.alert.msg[0]
assert no_purchase_fork.code == MessageCode.ORG_MAJORITY_NO_PURCHASE
assert no_purchase_fork.org.username == "org2"

@ -0,0 +1,25 @@
astroid==2.12.9
black==22.8.0
certifi==2022.9.14
charset-normalizer==2.1.1
click==8.1.3
coverage==6.4.4
dill==0.3.5.1
greenlet==1.1.3
idna==3.4
isort==5.10.1
jedi==0.18.1
lazy-object-proxy==1.7.1
mccabe==0.7.0
msgpack==1.0.4
mypy-extensions==0.4.3
parso==0.8.3
pathspec==0.10.1
platformdirs==2.5.2
pylint==2.15.2
pynvim==0.4.3
requests==2.28.1
tomli==2.0.1
tomlkit==0.11.4
urllib3==1.26.12
wrapt==1.14.1

@ -0,0 +1,9 @@
from setuptools import setup
setup(
name="check",
version="0.1",
install_requires=["pytest"],
packages=["check"],
package_data={"check": ["check/tests/*", "check/tests/**/*"]},
)
Loading…
Cancel
Save