feat: validate with user and org policy and tests
ci/woodpecker/push/woodpecker Pipeline failed
Details
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 resultsmaster
parent
6f1765329a
commit
9cac98368b
@ -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 @@
|
||||
|
@ -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
|
Loading…
Reference in New Issue