Skip to content

Commit 3d2db5c

Browse files
authored
Add a unit test for the Rules engine (#52)
* Add a unit test for the Rules engine * Fix path for GHA * Rules Engine linter
1 parent d2fb46e commit 3d2db5c

File tree

10 files changed

+125
-95
lines changed

10 files changed

+125
-95
lines changed

.github/workflows/unit_test_linter_rules_engine.yaml

+6-8
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ on:
1111
- main
1212
paths:
1313
- "components/common/**"
14-
- "components/rules_engine"
15-
- ".github/workflows/unit_test_rules_engine.yaml"
14+
- "components/rules_engine/**"
15+
- ".github/workflows/unit_test_linter_rules_engine.yaml"
1616
- ".pylintrc"
1717
- "!components/rules_engine/**.md"
1818
workflow_dispatch:
@@ -26,9 +26,7 @@ jobs:
2626
fail-fast: false
2727
matrix:
2828
python-version: [3.9]
29-
target-folder: [
30-
components/rules_engine,
31-
]
29+
target-folder: [components/rules_engine]
3230
# copier:raw
3331
steps:
3432
- uses: actions/checkout@v3
@@ -62,7 +60,7 @@ jobs:
6260
BASE_DIR=$(pwd)
6361
python -m pip install --upgrade pip
6462
python -m pip install pytest pytest-custom_exit_code pytest-cov pylint pytest-mock mock
65-
if [ -f $BASE_DIR/common/requirements.txt ]; then pip install -r $BASE_DIR/common/requirements.txt; fi
63+
if [ -f $BASE_DIR/components/common/requirements.txt ]; then pip install -r $BASE_DIR/components/common/requirements.txt; fi
6664
cd ${{ matrix.target-folder }}
6765
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
6866
if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi
@@ -71,15 +69,15 @@ jobs:
7169
run: |
7270
BASE_DIR=$(pwd)
7371
cd ${{ matrix.target-folder }}/src
74-
PYTEST_ADDOPTS="--cache-clear --cov . " PYTHONPATH=$BASE_DIR/common/src python -m pytest
72+
PYTEST_ADDOPTS="--cache-clear --cov . " PYTHONPATH=$BASE_DIR/components/common/src python -m pytest
7573
7674
linter:
7775
runs-on: ubuntu-latest
7876
strategy:
7977
fail-fast: false
8078
matrix:
8179
python-version: [3.9]
82-
target-folder: [common]
80+
target-folder: [components/rules_engine]
8381
steps:
8482
- uses: actions/checkout@v3
8583
- name: Set up Python ${{ matrix.python-version }}

components/rules_engine/src/config.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""
16+
Rules Service config file
17+
"""
1518

1619
import os
1720

1821
PORT = os.environ["PORT"] if os.environ.get("PORT") is not None else 80
19-
PROJECT_ID = os.environ.get("PROJECT_ID") or \
20-
os.environ.get("GOOGLE_CLOUD_PROJECT")
22+
PROJECT_ID = os.environ.get("PROJECT_ID",
23+
os.environ.get("GOOGLE_CLOUD_PROJECT"))
2124
DATABASE_PREFIX = os.getenv("DATABASE_PREFIX", "")
2225
SERVICE_NAME = os.getenv("SERVICE_NAME")

components/rules_engine/src/main.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# Rules RESTful Microservice
15+
"""Rules RESTful Microservice"""
1616

1717
import uvicorn
1818
import config
1919
from fastapi import FastAPI
2020
from fastapi.responses import HTMLResponse
21-
from google.cloud.firestore import Client
2221
from common.utils.http_exceptions import add_exception_handlers
2322

2423
from routes import rules, rulesets

components/rules_engine/src/models/rule.py

+5-16
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,20 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
# Data model for Rule. See https://octabyte.io/FireO/ for details.
1514

15+
"""Data model for Rule. See https://octabyte.io/FireO/ for details."""
1616

17-
import os
18-
19-
from fireo.models import Model
20-
from fireo.fields import IDField, TextField, BooleanField, DateTime, ListField
17+
from fireo.fields import IDField, TextField, DateTime, ListField
2118
from fireo.queries.errors import ReferenceDocNotExist
22-
from datetime import datetime
2319
from common.models.base_model import BaseModel
2420

25-
# GCP project_id from system's environment variable.
26-
PROJECT_ID = os.environ.get("PROJECT_ID", "")
27-
28-
# Database prefix for integration and e2e test purpose.
29-
DATABASE_PREFIX = os.getenv("DATABASE_PREFIX", "")
30-
31-
3221
# Firebase data model in "rules" collection.
3322
class Rule(BaseModel):
3423
"""Rule ORM class"""
3524

3625
class Meta:
3726
ignore_none_field = False
38-
collection_name = DATABASE_PREFIX + "rules"
27+
collection_name = BaseModel.DATABASE_PREFIX + "rules"
3928

4029
id = IDField()
4130
title = TextField()
@@ -46,9 +35,9 @@ class Meta:
4635
modified_at = DateTime()
4736

4837
@classmethod
49-
def find_by_doc_id(cls, id):
38+
def find_by_doc_id(cls, doc_id):
5039
try:
51-
rule = Rule.collection.get(f"rules/{id}")
40+
rule = Rule.collection.get(f"rules/{doc_id}")
5241
except ReferenceDocNotExist:
5342
return None
5443

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Unit test for Rule.py
17+
"""
18+
# disabling these rules, as they cause issues with pytest fixtures
19+
# pylint: disable=unused-import,unused-argument,redefined-outer-name
20+
from models.rule import Rule
21+
from common.testing.firestore_emulator import firestore_emulator, clean_firestore
22+
from datetime import datetime
23+
24+
25+
TEST_RULE = {
26+
"name": "Rule name",
27+
"title": "Rule Title",
28+
"type": "Rule Type",
29+
"description": "Rule Description",
30+
"fields": ["Foo", "Bar"],
31+
"sql_query": "select employee_name from employee",
32+
"created_at": datetime(year=2022, month=10, day=14),
33+
"modified_at": datetime(year=2022, month=12, day=25)
34+
}
35+
36+
def test_rule(clean_firestore):
37+
"""Test for creating, loading and deleting of a new rule"""
38+
new_rule = Rule.from_dict(TEST_RULE)
39+
new_rule.save()
40+
rule = Rule.find_by_id(new_rule.id)
41+
assert rule.title == TEST_RULE["title"]
42+
assert rule.description == TEST_RULE["description"]
43+
Rule.soft_delete_by_id(new_rule.id)

components/rules_engine/src/models/ruleset.py

+5-18
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,19 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
# Data model for Rule. See https://octabyte.io/FireO/ for details.
1514

15+
"""Firebase Data model for RuleSet"""
1616

17-
import os
18-
19-
from fireo.models import Model
20-
from fireo.fields import IDField, TextField, BooleanField, DateTime, ListField
17+
from fireo.fields import IDField, TextField, DateTime, ListField
2118
from fireo.queries.errors import ReferenceDocNotExist
22-
from datetime import datetime
2319
from common.models.base_model import BaseModel
24-
from rule import Rule
25-
26-
# GCP project_id from system's environment variable.
27-
PROJECT_ID = os.environ.get("PROJECT_ID", "")
28-
29-
# Database prefix for integration and e2e test purpose.
30-
DATABASE_PREFIX = os.getenv("DATABASE_PREFIX", "")
31-
3220

33-
# Firebase data model in "rules" collection.
3421
class RuleSet(BaseModel):
3522
"""RuleSet ORM class"""
3623

3724
class Meta:
3825
ignore_none_field = False
39-
collection_name = DATABASE_PREFIX + "rulesets"
26+
collection_name = BaseModel.DATABASE_PREFIX + "rulesets"
4027

4128
id = IDField()
4229
name = TextField()
@@ -47,9 +34,9 @@ class Meta:
4734
modified_at = DateTime()
4835

4936
@classmethod
50-
def find_by_doc_id(cls, id):
37+
def find_by_doc_id(cls, doc_id):
5138
try:
52-
ruleset = RuleSet.collection.get(f"rulesets/{id}")
39+
ruleset = RuleSet.collection.get(f"rulesets/{doc_id}")
5340
except ReferenceDocNotExist:
5441
return None
5542

components/rules_engine/src/routes/rules.py

+18-19
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,24 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
""" Rule endpoints """
16+
1517
from fastapi import APIRouter, HTTPException
1618
from models.rule import Rule
1719
from schemas.rule import RuleSchema
1820
import datetime
1921

20-
# disabling for linting to pass
21-
# pylint: disable = broad-except
22-
2322
router = APIRouter(prefix="/rule", tags=["rule"])
2423

2524
SUCCESS_RESPONSE = {"status": "Success"}
2625

2726

28-
@router.get("/{id}", response_model=RuleSchema)
29-
async def get(id: str):
27+
@router.get("/{rule_id}", response_model=RuleSchema)
28+
async def get(rule_id: str):
3029
"""Get a Rule
3130
3231
Args:
33-
id (str): unique id of the rule
32+
rule_id (str): unique id of the rule
3433
3534
Raises:
3635
HTTPException: 404 Not Found if rule doesn't exist for the given rule id
@@ -39,10 +38,10 @@ async def get(id: str):
3938
Returns:
4039
[rule]: rule object for the provided rule id
4140
"""
42-
rule = Rule.find_by_doc_id(id)
41+
rule = Rule.find_by_doc_id(rule_id)
4342

4443
if rule is None:
45-
raise HTTPException(status_code=404, detail=f"Rule {id} not found.")
44+
raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found.")
4645
return rule
4746

4847

@@ -59,12 +58,12 @@ async def post(data: RuleSchema):
5958
Returns:
6059
[JSON]: rule ID of the rule if the rule is successfully created
6160
"""
62-
id = data.id
63-
existing_rule = Rule.find_by_doc_id(id)
61+
rule_id = data.id
62+
existing_rule = Rule.find_by_doc_id(rule_id)
6463

6564
if existing_rule:
6665
raise HTTPException(status_code=409,
67-
detail=f"Rule {id} already exists.")
66+
detail=f"Rule {rule_id} already exists.")
6867

6968
new_rule = Rule()
7069
new_rule = new_rule.from_dict({**data.dict()})
@@ -88,26 +87,26 @@ async def put(data: RuleSchema):
8887
Returns:
8988
[JSON]: {'status': 'Succeed'} if the rule is updated
9089
"""
91-
id = data.id
92-
rule = Rule.find_by_doc_id(id)
90+
rule_id = data.id
91+
rule = Rule.find_by_doc_id(rule_id)
9392

9493
if rule:
9594
rule = rule.from_dict({**data.dict()})
9695
rule.modified_at = datetime.datetime.utcnow()
9796
rule.save()
9897

9998
else:
100-
raise HTTPException(status_code=404, detail=f"Rule {id} not found.")
99+
raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found.")
101100

102101
return SUCCESS_RESPONSE
103102

104103

105-
@router.delete("/{id}")
106-
async def delete(id: str):
104+
@router.delete("/{rule_id}")
105+
async def delete(rule_id: str):
107106
"""Delete a Rule
108107
109108
Args:
110-
id (str): unique id of the rule
109+
rule_id (str): unique id of the rule
111110
112111
Raises:
113112
HTTPException: 500 Internal Server Error if something fails
@@ -116,9 +115,9 @@ async def delete(id: str):
116115
[JSON]: {'status': 'Succeed'} if the rule is deleted
117116
"""
118117

119-
rule = Rule.find_by_doc_id(id)
118+
rule = Rule.find_by_doc_id(rule_id)
120119
if rule is None:
121-
raise HTTPException(status_code=404, detail=f"Rule {id} not found.")
120+
raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found.")
122121

123122
Rule.collection.delete(rule.key)
124123

0 commit comments

Comments
 (0)