Skip to content
Merged
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
- name: Install
working-directory: /home/runner/frappe-bench
run: |
bench get-app frappe_factory_bot https://github.com/harshtandiya/frappe_factory_bot.git --branch main
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify CI workflow does not use mutable branch refs for get-app.
rg -n --iglob '*.yml' --iglob '*.yaml' 'bench get-app .*--branch (main|master)\b' .github/workflows

# Verify source is not a personal fork namespace.
rg -n --iglob '*.yml' --iglob '*.yaml' 'bench get-app .*https://github.com/[a-zA-Z0-9_-]+/frappe_factory_bot\.git' .github/workflows

Repository: BuildWithHussain/forms_pro

Length of output: 349


Pin frappe_factory_bot to an immutable ref from a trusted source.

Line 93 uses a mutable main branch from a personal fork, which makes builds non-reproducible and weakens supply-chain safety. Pin to a version tag and use a trusted upstream repository.

Suggested change
-          bench get-app frappe_factory_bot https://github.com/harshtandiya/frappe_factory_bot.git --branch main
+          bench get-app frappe_factory_bot https://github.com/<trusted-org>/frappe_factory_bot.git --branch <pinned-tag>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 93, The workflow currently checks out a
mutable personal fork branch via the bench command ("bench get-app
frappe_factory_bot ... --branch main"); change this to pin to an immutable,
trusted ref by replacing the repository URL with the upstream/trusted repository
and using a specific tag or commit hash instead of "main" (e.g., use --branch
vX.Y.Z or --branch <commit-sha>) so builds are reproducible and supply-chain
safe; update the bench invocation that references frappe_factory_bot accordingly
and ensure the chosen tag/sha exists in the trusted upstream before committing.

bench get-app forms_pro $GITHUB_WORKSPACE
bench setup requirements --dev
bench new-site --db-root-password root --admin-password admin test_site
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ The project includes automated testing via GitHub Actions:
- **CI**: Installs the app and runs unit tests on pull requests and pushes to `develop`
- **Linters**: Runs [Frappe Semgrep Rules](https://github.com/frappe/semgrep-rules) and [pip-audit](https://pypi.org/project/pip-audit/) on every pull request

### Running Tests Locally

The test suite uses [frappe_factory_bot](https://github.com/harshtandiya/frappe_factory_bot) for test data factories. You must install it on your bench before running tests.

**1. Install frappe_factory_bot:**

```bash
cd $PATH_TO_YOUR_BENCH
bench get-app https://github.com/harshtandiya/frappe_factory_bot.git --branch main
bench --site <your-site> install-app frappe_factory_bot
```

**2. Run the tests:**

```bash
# Run all Forms Pro tests
bench --site <your-site> run-tests --app forms_pro

# Run a specific test module
bench --site <your-site> run-tests --module forms_pro.tests.test_roles
```

## 📄 License

This project is licensed under the **AGPL-3.0** License - see the [LICENSE](LICENSE) file for details.
Expand Down
27 changes: 13 additions & 14 deletions forms_pro/forms_pro/doctype/fp_team/test_fp_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from frappe.defaults import get_user_default
from frappe.tests import IntegrationTestCase

from forms_pro.tests import FORMS_PRO_TEST_USER
from forms_pro.tests.factories import FPTeamFactory, UserFactory

# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
Expand All @@ -22,7 +22,7 @@ class IntegrationTestFPTeam(IntegrationTestCase):

def setUp(self):
super().setUp()
self.test_user = FORMS_PRO_TEST_USER
frappe.set_user("Administrator")

def tearDown(self):
frappe.set_user("Administrator")
Expand All @@ -33,45 +33,44 @@ def test_add_owner_to_team(self):
Test that after a user creates a team, that owner user is added to the team and the team is shared with the owner user
"""

frappe.set_user(self.test_user)
owner = UserFactory.create("with_forms_pro_role")
frappe.set_user(owner.name)

team = frappe.new_doc("FP Team")
team.team_name = "Test Team"
team.insert()
team = FPTeamFactory.create()
team.reload()

frappe.set_user("Administrator")

# Check that the user is added to the team
self.assertTrue(team.is_team_member(self.test_user))
# Check that the owner is added to the team
self.assertTrue(team.is_team_member(owner.name))

# Check that the user is added to the team
# Check that the FP Team Member row exists
self.assertIsNotNone(
frappe.db.exists(
"FP Team Member",
{
"parent": team.name,
"parentfield": "users",
"parenttype": "FP Team",
"user": self.test_user,
"user": owner.name,
},
)
)

# Check that the user is added to the team's docshare
# Check that the team is shared with the owner
self.assertTrue(
frappe.db.exists(
"DocShare",
{
"share_doctype": "FP Team",
"share_name": team.name,
"user": self.test_user,
"user": owner.name,
"read": 1,
"write": 1,
"share": 1,
},
)
)

# Check that the user's current team is set to the team
self.assertEqual(get_user_default("current_team", self.test_user), team.name)
# Check that the owner's current team is set to this team
self.assertEqual(get_user_default("current_team", owner.name), team.name)
5 changes: 0 additions & 5 deletions forms_pro/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
fixtures = [
{"dt": "Role", "filters": {"role_name": "Forms Pro User"}},
]
# Apps
# ------------------

# required_apps = []

# Each item in the list will be shown as an app in the apps page
add_to_apps_screen = [
{
Expand Down
4 changes: 4 additions & 0 deletions forms_pro/tests/factories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from forms_pro.tests.factories.fp_team_factory import FPTeamFactory
from forms_pro.tests.factories.user_factory import UserFactory

__all__ = ["FPTeamFactory", "UserFactory"]
14 changes: 14 additions & 0 deletions forms_pro/tests/factories/fp_team_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Any

from faker import Faker
from frappe_factory_bot.frappe_factory_bot.base_factory import BaseFactory

_fake = Faker()


class FPTeamFactory(BaseFactory):
doctype = "FP Team"

@property
def default_attributes(self) -> dict[str, Any]:
return {"team_name": f"{_fake.word().capitalize()} Team"}
26 changes: 26 additions & 0 deletions forms_pro/tests/factories/user_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Any

from faker import Faker
from frappe_factory_bot.frappe_factory_bot.base_factory import BaseFactory

from forms_pro.roles import FORMS_PRO_ROLE

_fake = Faker()


class UserFactory(BaseFactory):
doctype = "User"

@property
def default_attributes(self) -> dict[str, Any]:
return {
"email": _fake.unique.email(),
"first_name": _fake.first_name(),
"last_name": _fake.last_name(),
}

@property
def with_forms_pro_role(self) -> dict[str, Any]:
# Frappe accepts child table rows as dicts in the initial doc dict.
# on_update fires with the role already set, so the default team is created.
return {"roles": [{"role": FORMS_PRO_ROLE}]}
29 changes: 29 additions & 0 deletions forms_pro/tests/factories/user_invitation_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any

from faker import Faker
from frappe_factory_bot.frappe_factory_bot.base_factory import BaseFactory

from forms_pro.roles import FORMS_PRO_ROLE

_fake = Faker()


class UserInvitationFactory(BaseFactory):
doctype = "User Invitation"

@property
def default_attributes(self) -> dict[str, Any]:
return {
"email": self.overrides.get("email", _fake.unique.email()),
"redirect_to_path": self.overrides.get("redirect_to_path", "/forms"),
"invited_by": self.overrides.get("invited_by") or "Administrator",
"status": self.overrides.get("status") or "Pending",
}

@property
def to_forms_pro_app(self) -> dict[str, Any]:
return {
"app_name": "forms_pro",
"redirect_to_path": "/forms",
"roles": [{"role": FORMS_PRO_ROLE}],
}
Loading
Loading