feat: add initial code

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..5181c73
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+include = async_casbin_adapter/*
+omit = *migrations*, *tests*
\ No newline at end of file
diff --git a/.github/semantic.yml b/.github/semantic.yml
new file mode 100644
index 0000000..96f9233
--- /dev/null
+++ b/.github/semantic.yml
@@ -0,0 +1,2 @@
+# Always validate the PR title AND all the commits
+titleAndCommits: true
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..a6a3495
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,101 @@
+name: build
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ['3.10', '3.11', '3.12']
+        os: [ubuntu-latest, macOS-latest, windows-latest]
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v2
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install dependencies
+      run: |
+        pip install -r requirements_dev.txt
+        pip install coveralls
+        pip install pytest
+
+    - name: Run tests
+      run: coverage run -m django test --settings=tests.settings
+
+    - name: Upload coverage data to coveralls.io
+      run: coveralls --service=github
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        COVERALLS_FLAG_NAME: ${{ matrix.os }} - ${{ matrix.python-version }}
+        COVERALLS_PARALLEL: true
+
+  lint:
+    name: Run Linters
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Super-Linter
+        uses: github/super-linter@v4.9.2
+        env:
+          VALIDATE_ALL_CODEBASE: false
+          VALIDATE_PYTHON_BLACK: true
+          DEFAULT_BRANCH: master
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          LINTER_RULES_PATH: /
+          PYTHON_BLACK_CONFIG_FILE: pyproject.toml
+
+  coveralls:
+    name: Indicate completion to coveralls.io
+    needs: test
+    runs-on: ubuntu-latest
+    container: python:3-slim
+    steps:
+    - name: Finished
+      run: |
+        pip3 install --upgrade coveralls
+        coveralls --finish
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+  release:
+    name: Release
+    runs-on: ubuntu-latest
+    needs: [ test, coveralls ]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v2
+        with:
+          node-version: '20'
+
+      - name: Setup
+        run: npm install
+      
+      - name: Set up python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.12'
+      
+      - name: Release
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
+        run: npx semantic-release
diff --git a/.releaserc.json b/.releaserc.json
new file mode 100644
index 0000000..94e24cd
--- /dev/null
+++ b/.releaserc.json
@@ -0,0 +1,23 @@
+{
+  "branches": "master",
+  "plugins": [
+    "@semantic-release/commit-analyzer",
+    "@semantic-release/release-notes-generator",
+    "semantic-release-pypi",
+    "@semantic-release/github",
+    [
+      "@semantic-release/changelog",
+      {
+        "changelogFile": "CHANGELOG.md",
+        "changelogTitle": "# Semantic Versioning Changelog"
+      }
+    ],
+	  [
+      "@semantic-release/git",
+      {
+        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
+        "assets": ["CHANGELOG.md", "pyproject.toml"]
+      }
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/CHANGELOG.md
diff --git a/README.md b/README.md
index d1e97c4..1510125 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,95 @@
 # async-django-orm-adapter
-Async Django ORM Adapter for PyCasbin
+
+[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN)
+
+Asynchronous Django ORM Adapter is the async [Django](https://www.djangoproject.com/) [ORM](https://docs.djangoproject.com/en/3.0/ref/databases/) adapter for [PyCasbin](https://github.com/casbin/pycasbin). With this library, Casbin can load policy from Django ORM supported database or save policy to it.
+
+Based on [Officially Supported Databases](https://docs.djangoproject.com/en/3.0/ref/databases/), The current supported databases are:
+
+- PostgreSQL
+- MariaDB
+- MySQL
+- Oracle
+- SQLite
+- IBM DB2
+- Microsoft SQL Server
+- Firebird
+- ODBC
+
+## Installation
+
+```
+pip install casbin-async-django-orm-adapter
+```
+
+Add `async_casbin_adapter.apps.AsyncCasbinAdapterConfig` to your `INSTALLED_APPS`
+
+```python
+# settings.py
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+INSTALLED_APPS = [
+    ...
+    'async_casbin_adapter.apps.AsyncCasbinAdapterConfig',
+    ...
+]
+
+CASBIN_MODEL = os.path.join(BASE_DIR, 'casbin.conf')
+```
+
+To run schema migration, execute `python manage.py migrate async_casbin_adapter`
+
+## Simple Example
+
+```python
+# views.py
+from async_casbin_adapter.enforcer import get_enforcer
+
+async def hello(request):
+    sub = "alice"  # the user that wants to access a resource.
+    obj = "data1"  # the resource that is going to be accessed.
+    act = "read"  # the operation that the user performs on the resource.
+
+    enforcer = await get_enforcer()
+    if e.enforce(sub, obj, act):
+        # permit alice to read data1casbin_django_orm_adapter
+        pass
+    else:
+        # deny the request, show an error
+        pass
+```
+
+## Configuration
+
+### `CASBIN_MODEL`
+A string containing the file location of your casbin model.
+
+### `CASBIN_ADAPTER`
+A string containing the adapter import path. Default to the django adapter shipped with this package: `async_casbin_adapter.adapter.AsyncAdapter`
+
+### `CASBIN_ADAPTER_ARGS`
+A tuple of arguments to be passed into the constructor of the adapter specified
+in `CASBIN_ADAPTER`. Refer to adapters to see available arguments. 
+
+E.g. if you wish to use the file adapter 
+set the adapter to `casbin.persist.adapters.FileAdapter` and use
+`CASBIN_ADAPTER_ARGS = ('path/to/policy_file.csv',)`
+
+### `CASBIN_DB_ALIAS`
+The database the adapter uses. Default to "default".
+
+### `CASBIN_WATCHER`
+Watcher instance to be set as the watcher on the enforcer instance.
+
+### `CASBIN_ROLE_MANAGER`
+Role manager instance to be set as the role manager on the enforcer instance.
+
+
+### Getting Help
+
+- [PyCasbin](https://github.com/casbin/pycasbin)
+
+### License
+
+This project is licensed under the [Apache 2.0 license](LICENSE).
+
diff --git a/async_casbin_adapter/__init__.py b/async_casbin_adapter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/async_casbin_adapter/__init__.py
diff --git a/async_casbin_adapter/adapter.py b/async_casbin_adapter/adapter.py
new file mode 100644
index 0000000..f0029ba
--- /dev/null
+++ b/async_casbin_adapter/adapter.py
@@ -0,0 +1,100 @@
+# Copyright 2025 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from casbin import persist
+from casbin.persist.adapters.asyncio import AsyncAdapter
+from django.db.utils import OperationalError, ProgrammingError
+
+from .models import CasbinRule
+
+logger = logging.getLogger(__name__)
+
+
+class AsyncAdapter(AsyncAdapter):
+    """the interface for Casbin async adapters."""
+
+    def __init__(self, db_alias="default"):
+        self.db_alias = db_alias
+
+    async def load_policy(self, model):
+        """loads all policy rules from the storage."""
+        try:
+            lines = CasbinRule.objects.using(self.db_alias).all()
+
+            async for line in lines:
+                persist.load_policy_line(str(line), model)
+        except (OperationalError, ProgrammingError) as error:
+            logger.warning("Could not load policy from database: {}".format(error))
+
+    def _create_policy_line(self, ptype, rule):
+        line = CasbinRule(ptype=ptype)
+        if len(rule) > 0:
+            line.v0 = rule[0]
+        if len(rule) > 1:
+            line.v1 = rule[1]
+        if len(rule) > 2:
+            line.v2 = rule[2]
+        if len(rule) > 3:
+            line.v3 = rule[3]
+        if len(rule) > 4:
+            line.v4 = rule[4]
+        if len(rule) > 5:
+            line.v5 = rule[5]
+        return line
+
+    async def save_policy(self, model):
+        """saves all policy rules to the storage."""
+        # this will delete all rules
+        await CasbinRule.objects.using(self.db_alias).all().adelete()
+
+        lines = []
+        for sec in ["p", "g"]:
+            if sec not in model.model.keys():
+                continue
+            for ptype, ast in model.model[sec].items():
+                for rule in ast.policy:
+                    lines.append(self._create_policy_line(ptype, rule))
+
+        db_alias = self.db_alias if self.db_alias else "default"
+        rows_created = await CasbinRule.objects.using(db_alias).abulk_create(lines)
+        return len(rows_created) > 0
+
+    async def add_policy(self, sec, ptype, rule):
+        """adds a policy rule to the storage."""
+        line = self._create_policy_line(ptype, rule)
+        await line.asave()
+
+    async def remove_policy(self, sec, ptype, rule):
+        """removes a policy rule from the storage."""
+        query_params = {"ptype": ptype}
+        for i, v in enumerate(rule):
+            query_params["v{}".format(i)] = v
+
+        rows_deleted, _ = await CasbinRule.objects.using(self.db_alias).filter(**query_params).adelete()
+        return rows_deleted > 0
+
+    async def remove_filtered_policy(self, sec, ptype, field_index, *field_values):
+        """removes policy rules that match the filter from the storage."""
+        query_params = {"ptype": ptype}
+        if not (0 <= field_index <= 5):
+            return False
+        if not (1 <= field_index + len(field_values) <= 6):
+            return False
+        for i, v in enumerate(field_values):
+            query_params["v{}".format(i + field_index)] = v
+
+        rows_deleted, _ = await CasbinRule.objects.using(self.db_alias).filter(**query_params).adelete()
+        return rows_deleted > 0
diff --git a/async_casbin_adapter/apps.py b/async_casbin_adapter/apps.py
new file mode 100644
index 0000000..3b1c625
--- /dev/null
+++ b/async_casbin_adapter/apps.py
@@ -0,0 +1,24 @@
+# Copyright 2025 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.apps import AppConfig
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+class AsyncCasbinAdapterConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "async_casbin_adapter"
diff --git a/async_casbin_adapter/enforcer.py b/async_casbin_adapter/enforcer.py
new file mode 100644
index 0000000..cdcde0b
--- /dev/null
+++ b/async_casbin_adapter/enforcer.py
@@ -0,0 +1,81 @@
+# Copyright 2025 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+from django.conf import settings
+from django.db import connections
+from asgiref.sync import sync_to_async
+
+from casbin import AsyncEnforcer
+
+from .utils import import_class
+
+
+logger = logging.getLogger(__name__)
+
+
+def _perform_sync_db_check(db_alias):
+    logger.info(f"Performing synchronous DB check for migration on alias '{db_alias}'...")
+    connection = connections[db_alias]
+    try:
+        with connection.cursor() as cursor:
+            cursor.execute(
+                """
+                SELECT app, name FROM django_migrations
+                WHERE app = 'async_casbin_adapter' AND name = '0001_initial';
+                """
+            )
+            row = cursor.fetchone()
+
+        if not row:
+            raise RuntimeError("Async Casbin Adapter initial migration not applied. ")
+        logger.info("Synchronous DB check passed.")
+        return True
+    except Exception as e:
+        logger.error(f"Synchronous DB check failed: {e}")
+        raise
+
+
+async def get_enforcer(db_alias=None):
+    """
+    An asynchronous factory function for creating and initializing Casbin AsyncEnforcer
+    """
+    if db_alias is None:
+        db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default")
+
+    async_db_check = sync_to_async(_perform_sync_db_check, thread_sensitive=True)
+    await async_db_check(db_alias)
+
+    model = getattr(settings, "CASBIN_MODEL")
+    adapter_loc = getattr(settings, "ASYNC_CASBIN_ADAPTER", "async_casbin_adapter.adapter.AsyncAdapter")
+    adapter_args = getattr(settings, "CASBIN_ADAPTER_ARGS", tuple())
+    Adapter = import_class(adapter_loc)
+    adapter = Adapter(db_alias, *adapter_args)
+
+    enforcer = AsyncEnforcer(model, adapter)
+
+    watcher_loc = getattr(settings, "CASBIN_WATCHER", None)
+    if watcher_loc:
+        Watcher = import_class(watcher_loc)
+        enforcer.set_watcher(Watcher())
+
+    role_manager_loc = getattr(settings, "CASBIN_ROLE_MANAGER", None)
+    if role_manager_loc:
+        RoleManager = import_class(role_manager_loc)
+        enforcer.set_role_manager(RoleManager())
+
+    await enforcer.load_policy()
+
+    logger.info("Casbin AsyncEnforcer initialized successfully.")
+    return enforcer
diff --git a/async_casbin_adapter/migrations/0001_initial.py b/async_casbin_adapter/migrations/0001_initial.py
new file mode 100644
index 0000000..20d0441
--- /dev/null
+++ b/async_casbin_adapter/migrations/0001_initial.py
@@ -0,0 +1,37 @@
+# Generated by Django 5.2.5 on 2025-09-05 09:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = []
+
+    operations = [
+        migrations.CreateModel(
+            name="CasbinRule",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("ptype", models.CharField(max_length=255)),
+                ("v0", models.CharField(max_length=255)),
+                ("v1", models.CharField(max_length=255)),
+                ("v2", models.CharField(max_length=255)),
+                ("v3", models.CharField(max_length=255)),
+                ("v4", models.CharField(max_length=255)),
+                ("v5", models.CharField(max_length=255)),
+            ],
+            options={
+                "db_table": "casbin_rule",
+            },
+        ),
+    ]
diff --git a/async_casbin_adapter/migrations/__init__.py b/async_casbin_adapter/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/async_casbin_adapter/migrations/__init__.py
diff --git a/async_casbin_adapter/models.py b/async_casbin_adapter/models.py
new file mode 100644
index 0000000..4551c59
--- /dev/null
+++ b/async_casbin_adapter/models.py
@@ -0,0 +1,48 @@
+# Copyright 2025 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.db import models
+
+
+class CasbinRule(models.Model):
+    ptype = models.CharField(max_length=255)
+    v0 = models.CharField(max_length=255)
+    v1 = models.CharField(max_length=255)
+    v2 = models.CharField(max_length=255)
+    v3 = models.CharField(max_length=255)
+    v4 = models.CharField(max_length=255)
+    v5 = models.CharField(max_length=255)
+
+    class Meta:
+        db_table = "casbin_rule"
+
+    def __str__(self):
+        text = self.ptype
+
+        if self.v0:
+            text = text + ", " + self.v0
+        if self.v1:
+            text = text + ", " + self.v1
+        if self.v2:
+            text = text + ", " + self.v2
+        if self.v3:
+            text = text + ", " + self.v3
+        if self.v4:
+            text = text + ", " + self.v4
+        if self.v5:
+            text = text + ", " + self.v5
+        return text
+
+    def __repr__(self):
+        return '<CasbinRule {}: "{}">'.format(self.id, str(self))
diff --git a/async_casbin_adapter/utils.py b/async_casbin_adapter/utils.py
new file mode 100644
index 0000000..71acf7a
--- /dev/null
+++ b/async_casbin_adapter/utils.py
@@ -0,0 +1,25 @@
+# Copyright 2025 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib
+
+
+def import_class(name):
+    """Import class from string
+    e.g. `package.module.ClassToImport` returns the `ClassToImport` class"""
+    components = name.split(".")
+    module_name = ".".join(components[:-1])
+    class_name = components[-1]
+    module = importlib.import_module(module_name)
+    return getattr(module, class_name)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..16bdf0f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+    "devDependencies": {
+        "@semantic-release/changelog": "^6.0.3",
+        "@semantic-release/git": "^10.0.1",
+        "semantic-release": "^22.0.5",
+        "semantic-release-pypi": "^5.1.0"
+    }
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..fbdb8af
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,47 @@
+[project]
+name = "casbin-async-django-orm-adapter"
+version = "0.1.0"
+authors = [
+    {name = "Yang Luo", email = "hsluoyz@qq.com"},
+]
+description = "Async Django ORM adapter for PyCasbin"
+readme = "README.md"
+keywords = [
+    "async",
+    "pycasbin",
+    "casbin",
+    "adapter",
+    "storage-driver",
+    "django",
+    "orm",
+    "django-orm",
+    "access-control",
+    "authorization"
+]
+dynamic = ["dependencies"]
+requires-python = ">=3.7"
+license = {text = "Apache 2.0"}
+classifiers = [
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "License :: OSI Approved :: Apache Software License",
+    "Operating System :: OS Independent",
+]
+
+[project.urls]
+Home-page = "https://github.com/officialpycasbin/async-django-orm-adapter"
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+exclude = ["tests"]
+
+[tool.setuptools.dynamic]
+dependencies = {file = ["requirements.txt"]}
+
+[tool.black]
+line-length = 120
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4c1bf56
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pycasbin>=2.0.0
+Django
\ No newline at end of file
diff --git a/requirements_dev.txt b/requirements_dev.txt
new file mode 100644
index 0000000..d22c302
--- /dev/null
+++ b/requirements_dev.txt
@@ -0,0 +1,2 @@
+-r requirements.txt
+setuptools
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/rbac_model.conf b/tests/rbac_model.conf
new file mode 100644
index 0000000..9ca4b92
--- /dev/null
+++ b/tests/rbac_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
diff --git a/tests/rbac_policy.csv b/tests/rbac_policy.csv
new file mode 100644
index 0000000..4150272
--- /dev/null
+++ b/tests/rbac_policy.csv
@@ -0,0 +1,6 @@
+p, alice, data1, read
+p, bob, data2, write
+p, data2_admin, data2, read
+p, data2_admin, data2, write
+
+g, alice, data2_admin
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..29e2069
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,45 @@
+import os
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+SECRET_KEY = "not-a-production-secret"
+
+INSTALLED_APPS = [
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "async_casbin_adapter.apps.AsyncCasbinAdapterConfig",
+    "tests",
+]
+
+MIDDLEWARE = [
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+            ]
+        },
+    }
+]
+
+DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
+
+CASBIN_MODEL = os.path.join(BASE_DIR, "tests", "rbac_model.conf")
diff --git a/tests/test_adapter.py b/tests/test_adapter.py
new file mode 100644
index 0000000..49aac71
--- /dev/null
+++ b/tests/test_adapter.py
@@ -0,0 +1,135 @@
+import os
+from casbin import AsyncEnforcer
+import simpleeval
+
+from django.test import TestCase
+from async_casbin_adapter.models import CasbinRule
+from async_casbin_adapter.adapter import AsyncAdapter
+
+
+def get_fixture(path):
+    dir_path = os.path.split(os.path.realpath(__file__))[0] + "/"
+    return os.path.abspath(dir_path + path)
+
+
+async def get_async_enforcer():
+    adapter = AsyncAdapter()
+
+    await CasbinRule.objects.abulk_create(
+        [
+            CasbinRule(ptype="p", v0="alice", v1="data1", v2="read"),
+            CasbinRule(ptype="p", v0="bob", v1="data2", v2="write"),
+            CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="read"),
+            CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="write"),
+            CasbinRule(ptype="g", v0="alice", v1="data2_admin"),
+        ]
+    )
+
+    enforcer = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+    await enforcer.load_policy()
+    return enforcer
+
+
+class TestAsyncAdapter(TestCase):
+    async def test_enforcer_basic(self):
+        e = await get_async_enforcer()
+        self.assertTrue(e.enforce("alice", "data1", "read"))
+        self.assertFalse(e.enforce("bob", "data1", "read"))
+        self.assertTrue(e.enforce("bob", "data2", "write"))
+        self.assertTrue(e.enforce("alice", "data2", "read"))
+        self.assertTrue(e.enforce("alice", "data2", "write"))
+
+    async def test_add_policy(self):
+        adapter = AsyncAdapter()
+        e = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+
+        try:
+            self.assertFalse(e.enforce("alice", "data1", "read"))
+            self.assertFalse(e.enforce("bob", "data1", "read"))
+            self.assertFalse(e.enforce("bob", "data2", "write"))
+            self.assertFalse(e.enforce("alice", "data2", "read"))
+            self.assertFalse(e.enforce("alice", "data2", "write"))
+        except simpleeval.NameNotDefined:
+            pass
+
+        await adapter.add_policy(sec="p", ptype="p", rule=["alice", "data1", "read"])
+        await adapter.add_policy(sec="p", ptype="p", rule=["bob", "data2", "write"])
+        await adapter.add_policy(sec="p", ptype="p", rule=["data2_admin", "data2", "read"])
+        await adapter.add_policy(sec="p", ptype="p", rule=["data2_admin", "data2", "write"])
+        await adapter.add_policy(sec="g", ptype="g", rule=["alice", "data2_admin"])
+
+        await e.load_policy()
+
+        self.assertTrue(e.enforce("alice", "data1", "read"))
+        self.assertFalse(e.enforce("bob", "data1", "read"))
+        self.assertTrue(e.enforce("bob", "data2", "write"))
+        self.assertTrue(e.enforce("alice", "data2", "read"))
+        self.assertTrue(e.enforce("alice", "data2", "write"))
+        self.assertFalse(e.enforce("bogus", "data2", "write"))
+
+    async def test_save_policy(self):
+        enforcer_for_model = AsyncEnforcer(get_fixture("rbac_model.conf"), get_fixture("rbac_policy.csv"))
+        await enforcer_for_model.load_policy()
+        model = enforcer_for_model.model
+
+        adapter = AsyncAdapter()
+        await adapter.save_policy(model)
+        e = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+        await e.load_policy()
+
+        self.assertTrue(e.enforce("alice", "data1", "read"))
+        self.assertFalse(e.enforce("bob", "data1", "read"))
+        self.assertTrue(e.enforce("bob", "data2", "write"))
+        self.assertTrue(e.enforce("alice", "data2", "read"))
+        self.assertTrue(e.enforce("alice", "data2", "write"))
+
+    async def test_autosave_off_doesnt_persist_to_db(self):
+        adapter = AsyncAdapter()
+        e = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+
+        e.enable_auto_save(False)
+        await e.add_policy("alice", "data1", "write")
+        await e.load_policy()
+        policies = e.get_policy()
+
+        self.assertListEqual(policies, [])
+
+    async def test_autosave_on_persists_to_db(self):
+        adapter = AsyncAdapter()
+        e = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+
+        e.enable_auto_save(True)
+        await e.add_policy("alice", "data1", "write")
+        await e.load_policy()
+        policies = e.get_policy()
+        self.assertListEqual(policies, [["alice", "data1", "write"]])
+
+    async def test_autosave_on_persists_remove_action_to_db(self):
+        adapter = AsyncAdapter()
+        e = AsyncEnforcer(get_fixture("rbac_model.conf"), adapter)
+        await e.add_policy("alice", "data1", "write")
+        await e.load_policy()
+        self.assertListEqual(e.get_policy(), [["alice", "data1", "write"]])
+
+        e.enable_auto_save(True)
+        await e.remove_policy("alice", "data1", "write")
+        await e.load_policy()
+        self.assertListEqual(e.get_policy(), [])
+
+    async def test_remove_filtered_policy(self):
+        e = await get_async_enforcer()
+
+        await e.remove_filtered_policy(0, "data2_admin")
+        await e.load_policy()
+        self.assertListEqual(e.get_policy(), [["alice", "data1", "read"], ["bob", "data2", "write"]])
+
+    def test_str(self):
+        rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read")
+        self.assertEqual(str(rule), "p, alice, data1, read")
+
+    async def test_repr(self):
+        rule = CasbinRule(ptype="p", v0="alice", v1="data1", v2="read")
+        self.assertEqual(repr(rule), '<CasbinRule None: "p, alice, data1, read">')
+
+        await rule.asave()
+        self.assertRegex(repr(rule), r'<CasbinRule \d+: "p, alice, data1, read">')
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
new file mode 100644
index 0000000..22b96e3
--- /dev/null
+++ b/tests/test_enforcer.py
@@ -0,0 +1,49 @@
+import os
+from django.test import TestCase
+from async_casbin_adapter.enforcer import get_enforcer
+from async_casbin_adapter.models import CasbinRule
+
+
+def get_fixture(path):
+    dir_path = os.path.split(os.path.realpath(__file__))[0] + "/"
+    return os.path.abspath(dir_path + path)
+
+
+async def db_setup():
+    await CasbinRule.objects.abulk_create(
+        [
+            CasbinRule(ptype="p", v0="alice", v1="data1", v2="read"),
+            CasbinRule(ptype="p", v0="bob", v1="data2", v2="write"),
+            CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="read"),
+            CasbinRule(ptype="p", v0="data2_admin", v1="data2", v2="write"),
+            CasbinRule(ptype="g", v0="alice", v1="data2_admin"),
+        ]
+    )
+
+
+class TestEnforcer(TestCase):
+    async def test_get_enforcer_basic(self):
+        enforcer = await get_enforcer()
+        await enforcer.add_policy("alice", "data1", "read")
+        self.assertEqual(enforcer.get_policy(), [["alice", "data1", "read"]])
+        self.assertTrue(enforcer.enforce("alice", "data1", "read"))
+
+    async def test_get_enforcer(self):
+        await db_setup()
+        enforcer = await get_enforcer()
+
+        self.assertEqual(
+            enforcer.get_policy(),
+            [
+                ["alice", "data1", "read"],
+                ["bob", "data2", "write"],
+                ["data2_admin", "data2", "read"],
+                ["data2_admin", "data2", "write"],
+            ],
+        )
+        self.assertTrue(enforcer.enforce("alice", "data1", "read"))
+        self.assertFalse(enforcer.enforce("bob", "data1", "read"))
+        self.assertTrue(enforcer.enforce("bob", "data2", "write"))
+        self.assertTrue(enforcer.enforce("alice", "data2", "read"))
+        self.assertTrue(enforcer.enforce("alice", "data2", "write"))
+        self.assertFalse(enforcer.enforce("bogus", "data2", "write"))