Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7522569ce0 | |||
| 351c696ef8 | |||
| 579dbdee10 | |||
| b4a877381a | |||
| 92753082c7 | |||
| 4df4172151 | |||
| e7af1fd591 | |||
| 8a2b0d0a76 | |||
| d48a3ff677 | |||
| 4fb5d87df9 | |||
| 6274764f90 | |||
| 7f0e231474 | |||
| 2c15609c1e | |||
| cd94dc5091 | |||
| 9d9216cf39 | |||
| 7dd9eae5d9 | |||
| 914319c366 | |||
| c34ad00fae | |||
| 289879962b | |||
| cd88799943 | |||
| 4011c8e8ed | |||
| b98281fa72 | |||
| 04bd154bad | |||
| 227ecd0700 | |||
| 6d618b4aff | |||
| 3a2b2b8a53 | |||
| f969b118cb | |||
| c48957612b | |||
| e09e1d1b80 |
@@ -11,26 +11,9 @@ jobs:
|
||||
notify-argocd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "people,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/people/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
|
||||
@@ -19,20 +19,9 @@ jobs:
|
||||
trivy-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "people,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -57,36 +46,19 @@ jobs:
|
||||
build-and-push-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "people,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/people-backend
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/people/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
- name: create-version-json
|
||||
id: create-version-json
|
||||
uses: jsdaniell/create-json@v1.2.3
|
||||
@@ -108,32 +80,15 @@ jobs:
|
||||
build-and-push-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "people,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/people-frontend
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/people/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
- name: create-version-json
|
||||
id: create-version-json
|
||||
uses: jsdaniell/create-json@v1.2.3
|
||||
@@ -144,7 +99,7 @@ jobs:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -163,29 +118,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "people,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/people/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/numerique-gouv/lasuite-deploiement"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
|
||||
|
||||
+31
-1
@@ -8,6 +8,34 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(api) add count mailboxes to MailDomain serializer
|
||||
- ✨(dimail) manage 'action required' status for MailDomain
|
||||
- ✨(domains) add action required status on MailDomain
|
||||
- ✨(dimail) send pending mailboxes upon domain activation
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🚑️(teams) do not display add button when disallowed #676
|
||||
- 🚑️(plugins) fix name from SIRET specific case #674
|
||||
- 🐛(api) restrict mailbox sync to enabled domains
|
||||
|
||||
## [1.10.1] - 2025-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(dimail) management command to fetch domain status
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(scripts) adapts release script after moving the deployment part
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(dimail) fix imported mailboxes should be enabled instead of pending #659
|
||||
- ⚡️(api) add missing cache for stats endpoint
|
||||
|
||||
## [1.10.0] - 2025-01-21
|
||||
|
||||
### Added
|
||||
@@ -21,6 +49,7 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) improve e2e tests avoiding race condition from mocks #641
|
||||
- 🐛(backend) fix flaky test with search contact #605
|
||||
- 🐛(backend) fix flaky test with team access #646
|
||||
- 🧑💻(dimail) remove 'NoneType: None' log in debug mode
|
||||
@@ -247,7 +276,8 @@ and this project adheres to
|
||||
- ✨(domains) create and manage domains on admin + API
|
||||
- ✨(domains) mailbox creation + link to email provisioning API
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.10.0...main
|
||||
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.10.1...main
|
||||
[1.10.1]: https://github.com/numerique-gouv/people/releases/v1.10.1
|
||||
[1.10.0]: https://github.com/numerique-gouv/people/releases/v1.10.0
|
||||
[1.9.1]: https://github.com/numerique-gouv/people/releases/v1.9.1
|
||||
[1.9.0]: https://github.com/numerique-gouv/people/releases/v1.9.0
|
||||
|
||||
@@ -403,3 +403,7 @@ install-secret: ## install the kubernetes secrets from Vaultwarden
|
||||
--set installCRDs=true; \
|
||||
fi
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
fetch-domain-status:
|
||||
@$(MANAGE) fetch_domain_status
|
||||
.PHONY: fetch-domain-status
|
||||
|
||||
+66
-25
@@ -24,18 +24,6 @@ def update_files(version):
|
||||
with open(path, 'w+') as file:
|
||||
file.writelines(lines)
|
||||
|
||||
# helm files
|
||||
sys.stdout.write("Update helm files...\n")
|
||||
for env in ["preprod", "production"]:
|
||||
path = f"src/helm/env.d/{env}/values.desk.yaml.gotmpl"
|
||||
with open(path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
for index, line in enumerate(lines):
|
||||
if "tag:" in line:
|
||||
lines[index] = re.sub(r'\"(.*?)\"', f'"v{version}"', line)
|
||||
with open(path, 'w+') as file:
|
||||
file.writelines(lines)
|
||||
|
||||
# frontend package.json
|
||||
sys.stdout.write("Update package.json...\n")
|
||||
files_to_modify = []
|
||||
@@ -55,6 +43,17 @@ def update_files(version):
|
||||
return
|
||||
|
||||
|
||||
def update_helm_files(path):
|
||||
sys.stdout.write("Update helm files...\n")
|
||||
with open(path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
for index, line in enumerate(lines):
|
||||
if "tag:" in line:
|
||||
lines[index] = re.sub(r'\"(.*?)\"', f'"v{version}"', line)
|
||||
with open(path, 'w+') as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def update_changelog(path, version):
|
||||
"""Update changelog file with release info
|
||||
"""
|
||||
@@ -77,7 +76,43 @@ def update_changelog(path, version):
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def prepare_release(version, kind):
|
||||
def deployment_part(version, kind):
|
||||
""" Update helm file of preprod on deployment repository numerique-gouv/lasuite-deploiement
|
||||
"""
|
||||
# Move to lasuite-deploiement repository
|
||||
try:
|
||||
os.chdir('../lasuite-deploiement')
|
||||
except FileNotFoundError:
|
||||
sys.stdout.write("\033[1;31m[Error] You must have a clone of https://github.com/numerique-gouv/lasuite-deploiement\x1b[0m")
|
||||
sys.stdout.write("\033[0;32mPlease clone it and re-run script with option --only-deployment-part\x1b[0m")
|
||||
sys.stdout.write(" >>> python scripts/release.py --only-deployment-part \x1b[0m")
|
||||
else:
|
||||
run_command("git checkout main", shell=True)
|
||||
run_command("git pull", shell=True)
|
||||
deployment_branch = f"regie/{version}/preprod"
|
||||
run_command(f"git checkout -b {deployment_branch}", shell=True)
|
||||
run_command("git pull --rebase origin main", shell=True)
|
||||
path = f"manifests/regie/env.d/preprod/values.desk.yaml.gotmpl"
|
||||
update_helm_files(path)
|
||||
run_command(f"git add {path}", shell=True)
|
||||
message = f"""🔖({RELEASE_KINDS[kind]}) release version {version}"""
|
||||
run_command(f"git commit -m '{message}'", shell=True)
|
||||
confirm = input(f"""\033[0;32m
|
||||
### DEPLOYMENT ###
|
||||
NEXT COMMAND on lasuite-deploiement repository:
|
||||
>> git push origin {deployment_branch}
|
||||
Continue ? (y,n)
|
||||
\x1b[0m""")
|
||||
if confirm == 'y':
|
||||
run_command(f"git push origin {deployment_branch}", shell=True)
|
||||
sys.stdout.write(f"""\033[1;34m
|
||||
PLEASE DO THE FOLLOWING INSTRUCTIONS:
|
||||
--> Please submit PR {deployment_branch} and merge code to main
|
||||
\x1b[0m""")
|
||||
|
||||
|
||||
def project_part(version, kind):
|
||||
"""Manage only la regie part with update of CHANGELOG, version files and tag"""
|
||||
sys.stdout.write('Let\'s go to create branch to release\n')
|
||||
branch_to_release = f"release/{version}"
|
||||
run_command(f"git checkout -b {branch_to_release}", shell=True)
|
||||
@@ -90,25 +125,29 @@ def prepare_release(version, kind):
|
||||
run_command("git add src/", shell=True)
|
||||
message = f"""🔖({RELEASE_KINDS[kind]}) release version {version}
|
||||
|
||||
Update all version files and changelog for {RELEASE_KINDS[kind]} release."""
|
||||
Update all version files and changelog for {RELEASE_KINDS[kind]} release."""
|
||||
run_command(f"git commit -m '{message}'", shell=True)
|
||||
confirm = input(f"\nNEXT COMMAND: \n>> git push origin {branch_to_release}\nContinue ? (y,n) ")
|
||||
confirm = input(f"""\033[0;32m
|
||||
### RELEASE ###
|
||||
NEXT COMMAND on current repository:
|
||||
>> git push origin {branch_to_release}
|
||||
Continue ? (y,n)
|
||||
\x1b[0m""")
|
||||
if confirm == 'y':
|
||||
run_command(f"git push origin {branch_to_release}", shell=True)
|
||||
sys.stdout.write(f"""
|
||||
PLEASE DO THE FOLLOWING INSTRUCTIONS:
|
||||
--> Please submit PR and merge code to main
|
||||
--> Then do:
|
||||
\033[1;34mPLEASE DO THE FOLLOWING INSTRUCTIONS:
|
||||
--> Please submit PR {branch_to_release} and merge code to main
|
||||
--> Then do:
|
||||
>> git checkout main
|
||||
>> git pull
|
||||
>> git tag v{version}
|
||||
>> git push origin v{version}
|
||||
--> Please check and wait for the docker image v{version} to be published here: https://hub.docker.com/r/lasuite/people-backend/tags and https://hub.docker.com/r/lasuite/people-frontend/tags
|
||||
>> git tag -d preprod
|
||||
>> git tag preprod
|
||||
>> git push -f origin preprod
|
||||
--> Now please generate release on github interface for the current tag v{version}
|
||||
""")
|
||||
--> Please check and wait for the docker image v{version} to be published here:
|
||||
- https://hub.docker.com/r/lasuite/people-backend/tags
|
||||
- https://hub.docker.com/r/lasuite/people-frontend/tags
|
||||
--> Now please generate release on github interface for the current tag v{version}
|
||||
\x1b[0m""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -117,4 +156,6 @@ if __name__ == "__main__":
|
||||
version = input("Enter your release version:")
|
||||
while kind not in RELEASE_KINDS:
|
||||
kind = input("Enter kind of release (p:patch, m:minor, mj:major):")
|
||||
prepare_release(version, kind)
|
||||
if "--only-deployment-part" not in sys.argv:
|
||||
project_part(version, kind)
|
||||
deployment_part(version, kind)
|
||||
|
||||
@@ -7,6 +7,8 @@ from functools import reduce
|
||||
from django.conf import settings
|
||||
from django.db.models import OuterRef, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
from rest_framework import (
|
||||
decorators,
|
||||
@@ -28,8 +30,6 @@ from core.utils.raw_sql import gen_sql_filter_json_array
|
||||
|
||||
from mailbox_manager import models as domains_models
|
||||
|
||||
SIMILARITY_THRESHOLD = 0.04
|
||||
|
||||
|
||||
class NestedGenericViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
@@ -303,7 +303,7 @@ class TeamViewSet(
|
||||
):
|
||||
"""Team ViewSet"""
|
||||
|
||||
permission_classes = [permissions.AccessPermission]
|
||||
permission_classes = [permissions.TeamPermission, permissions.AccessPermission]
|
||||
serializer_class = serializers.TeamSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["created_at", "name", "path"]
|
||||
@@ -596,19 +596,20 @@ class StatView(views.APIView):
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@method_decorator(cache_page(3600))
|
||||
def get(self, request):
|
||||
"""
|
||||
GET /api/v1.0/stats/
|
||||
Return a dictionary of public metrics.
|
||||
"""
|
||||
context = {
|
||||
"total_users": models.User.objects.all().count(),
|
||||
"total_users": models.User.objects.count(),
|
||||
"mau": models.User.objects.filter(
|
||||
last_login__gte=datetime.datetime.now() - datetime.timedelta(30)
|
||||
).count(),
|
||||
"teams": models.Team.objects.all().count(),
|
||||
"domains": domains_models.MailDomain.objects.all().count(),
|
||||
"mailboxes": domains_models.Mailbox.objects.all().count(),
|
||||
"teams": models.Team.objects.count(),
|
||||
"domains": domains_models.MailDomain.objects.count(),
|
||||
"mailboxes": domains_models.Mailbox.objects.count(),
|
||||
}
|
||||
return response.Response(context)
|
||||
|
||||
|
||||
@@ -53,3 +53,18 @@ class AccessPermission(IsAuthenticated):
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
return abilities.get(request.method.lower(), False)
|
||||
|
||||
|
||||
class TeamPermission(IsAuthenticated):
|
||||
"""Permission class for team objects viewset."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check permission only when the user tries to create a new team."""
|
||||
if not super().has_permission(request, view):
|
||||
return False
|
||||
|
||||
if request.method != "POST":
|
||||
return True
|
||||
|
||||
abilities = request.user.get_abilities()
|
||||
return abilities["teams"]["can_create"]
|
||||
|
||||
@@ -21,8 +21,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext, override
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
import jsonschema
|
||||
from timezone_field import TimeZoneField
|
||||
@@ -573,7 +573,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
.get()
|
||||
)
|
||||
|
||||
teams_can_view = user_info.teams_can_view
|
||||
teams_can_view = user_info.teams_can_view or settings.FEATURES["TEAMS_DISPLAY"]
|
||||
mailboxes_can_view = user_info.mailboxes_can_view
|
||||
|
||||
return {
|
||||
@@ -585,7 +585,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
),
|
||||
},
|
||||
"teams": {
|
||||
"can_view": teams_can_view and settings.FEATURES["TEAMS_DISPLAY"],
|
||||
"can_view": teams_can_view,
|
||||
"can_create": teams_can_view and settings.FEATURES["TEAMS_CREATE"],
|
||||
},
|
||||
"mailboxes": {
|
||||
@@ -961,14 +961,15 @@ class Invitation(BaseModel):
|
||||
"""Email invitation to the user."""
|
||||
try:
|
||||
with override(self.issuer.language):
|
||||
subject = gettext("Invitation to join La Régie!")
|
||||
template_vars = {
|
||||
"title": _("Invitation to join Desk!"),
|
||||
"title": subject,
|
||||
"site": Site.objects.get_current(),
|
||||
}
|
||||
msg_html = render_to_string("mail/html/invitation.html", template_vars)
|
||||
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||
mail.send_mail(
|
||||
_("Invitation to join Desk!"),
|
||||
subject,
|
||||
msg_plain,
|
||||
settings.EMAIL_FROM,
|
||||
[self.email],
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from rest_framework.status import (
|
||||
HTTP_201_CREATED,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_403_FORBIDDEN,
|
||||
)
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -28,7 +29,7 @@ def test_api_teams_create_anonymous():
|
||||
assert not Team.objects.exists()
|
||||
|
||||
|
||||
def test_api_teams_create_authenticated():
|
||||
def test_api_teams_create_authenticated(settings):
|
||||
"""
|
||||
Authenticated users should be able to create teams and should automatically be declared
|
||||
as the owner of the newly created team.
|
||||
@@ -39,6 +40,14 @@ def test_api_teams_create_authenticated():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.FEATURES = {
|
||||
"TEAMS_DISPLAY": True,
|
||||
"TEAMS_CREATE": True,
|
||||
"CONTACTS_DISPLAY": False,
|
||||
"CONTACTS_CREATE": False,
|
||||
"MAILBOXES_CREATE": False,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{
|
||||
@@ -54,6 +63,36 @@ def test_api_teams_create_authenticated():
|
||||
assert team.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
|
||||
def test_api_teams_create_authenticated_feature_disabled(settings):
|
||||
"""
|
||||
Authenticated users should not be able to create teams when feature is disabled.
|
||||
"""
|
||||
organization = OrganizationFactory(with_registration_id=True)
|
||||
user = UserFactory(organization=organization)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
settings.FEATURES = {
|
||||
"TEAMS_DISPLAY": True,
|
||||
"TEAMS_CREATE": False,
|
||||
"CONTACTS_DISPLAY": False,
|
||||
"CONTACTS_CREATE": False,
|
||||
"MAILBOXES_CREATE": False,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/teams/",
|
||||
{
|
||||
"name": "my team",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
assert not Team.objects.exists()
|
||||
|
||||
|
||||
def test_api_teams_create_cannot_override_organization():
|
||||
"""
|
||||
Authenticated users should be able to create teams and not
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Test stats endpoint
|
||||
"""
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
@@ -14,17 +16,34 @@ from mailbox_manager import models as domains_models
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_stats__anonymous():
|
||||
def test_api_stats__anonymous(django_assert_num_queries):
|
||||
"""Stats endpoint should be available even when not connected."""
|
||||
|
||||
response = APIClient().get("/api/v1.0/stats/")
|
||||
domains_factories.MailDomainFactory.create_batch(5)
|
||||
core_factories.TeamFactory.create_batch(3)
|
||||
|
||||
# clear cache to allow stats count
|
||||
cache.clear()
|
||||
with django_assert_num_queries(5):
|
||||
response = APIClient().get("/api/v1.0/stats/")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"total_users": 0,
|
||||
"mau": 0,
|
||||
"domains": 0,
|
||||
"domains": 5,
|
||||
"mailboxes": 0,
|
||||
"teams": 0,
|
||||
"teams": 3,
|
||||
}
|
||||
# no new request made due to caching
|
||||
with django_assert_num_queries(0):
|
||||
response = APIClient().get("/api/v1.0/stats/")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"total_users": 0,
|
||||
"mau": 0,
|
||||
"domains": 5,
|
||||
"mailboxes": 0,
|
||||
"teams": 3,
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +62,8 @@ def test_api_stats__expected_count():
|
||||
10, domain=domains_models.MailDomain.objects.all()[1]
|
||||
)
|
||||
|
||||
# clear cache to allow stats count
|
||||
cache.clear()
|
||||
response = APIClient().get("/api/v1.0/stats/")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
|
||||
@@ -248,7 +248,9 @@ def test_models_team_invitations_email():
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
factories.TeamAccessFactory(team=team)
|
||||
invitation = factories.InvitationFactory(team=team, email="john@people.com")
|
||||
invitation = factories.InvitationFactory(
|
||||
team=team, email="john@people.com", issuer__language="fr-fr"
|
||||
)
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
@@ -257,10 +259,10 @@ def test_models_team_invitations_email():
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == [invitation.email]
|
||||
assert email.subject == "Invitation to join Desk!"
|
||||
assert email.subject == "Invitation à rejoindre La Régie!"
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Desk!" in email_content
|
||||
assert "Invitation à rejoindre La Régie!" in email_content
|
||||
assert "[//example.com]" in email_content
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def test_api_users_retrieve_me_authenticated():
|
||||
"abilities": {
|
||||
"contacts": {"can_create": True, "can_view": True},
|
||||
"mailboxes": {"can_create": False, "can_view": False},
|
||||
"teams": {"can_create": False, "can_view": False},
|
||||
"teams": {"can_create": True, "can_view": True},
|
||||
},
|
||||
"organization": {
|
||||
"id": str(user.organization.pk),
|
||||
@@ -66,11 +66,18 @@ def test_api_users_retrieve_me_authenticated():
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_retrieve_me_authenticated_abilities():
|
||||
def test_api_users_retrieve_me_authenticated_abilities(settings):
|
||||
"""
|
||||
Authenticated users should be able to retrieve their own user via the "/users/me" path
|
||||
with the proper abilities.
|
||||
"""
|
||||
settings.FEATURES = {
|
||||
"TEAMS_DISPLAY": False,
|
||||
"TEAMS_CREATE": True,
|
||||
"CONTACTS_DISPLAY": True,
|
||||
"CONTACTS_CREATE": True,
|
||||
"MAILBOXES_CREATE": True,
|
||||
}
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-09 11:42+0000\n"
|
||||
"POT-Creation-Date: 2025-02-03 10:27+0000\n"
|
||||
"PO-Revision-Date: 2024-01-03 23:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
@@ -17,331 +17,359 @@ msgstr ""
|
||||
"X-Crowdin-File: backend.pot\n"
|
||||
"X-Crowdin-File-ID: 2\n"
|
||||
|
||||
#: core/admin.py:56
|
||||
#: core/admin.py:63
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
msgstr "Informations personnelles"
|
||||
|
||||
#: core/admin.py:58
|
||||
#: core/admin.py:65
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:70
|
||||
#: core/admin.py:77
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:109
|
||||
#: core/admin.py:116
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: core/admin.py:218
|
||||
msgid "Run post creation plugins"
|
||||
msgstr "Exécuter les plugins de post-création"
|
||||
|
||||
#: core/admin.py:226
|
||||
msgid "Post creation plugins have been run for the selected organizations."
|
||||
msgstr ""
|
||||
"Les plugins de post-création ont été exécutés pour les organisations "
|
||||
"sélectionnées."
|
||||
|
||||
#: core/authentication/backends.py:94
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
"Les informations de l'utilisateur ne contiennent aucune identification "
|
||||
"reconnaissable"
|
||||
|
||||
#: core/authentication/backends.py:116
|
||||
msgid "User account is disabled"
|
||||
msgstr ""
|
||||
msgstr "Le compte de l'utilisateur est désactivé"
|
||||
|
||||
#: core/authentication/backends.py:162
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
"Les claims ne contiennent aucune identification reconnaissable pour "
|
||||
"l'utilisateur"
|
||||
|
||||
#: core/authentication/backends.py:181
|
||||
msgid "Claims contained no recognizable organization identification"
|
||||
msgstr ""
|
||||
"Les claims ne contiennent aucune identification reconnaissable pour "
|
||||
"l'organisation"
|
||||
|
||||
#: core/enums.py:23
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
msgstr "En échec"
|
||||
|
||||
#: core/enums.py:24 mailbox_manager/enums.py:21 mailbox_manager/enums.py:30
|
||||
#: core/enums.py:24 mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
msgid "Pending"
|
||||
msgstr "En attente"
|
||||
|
||||
#: core/enums.py:25
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
msgstr "Réussi"
|
||||
|
||||
#: core/models.py:46
|
||||
#: core/models.py:47
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:47 core/models.py:59 mailbox_manager/enums.py:14
|
||||
#: core/models.py:48 core/models.py:60 mailbox_manager/enums.py:14
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:48 mailbox_manager/enums.py:15
|
||||
#: core/models.py:49 mailbox_manager/enums.py:15
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:71
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:72
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
msgid "id"
|
||||
msgstr "identifiant"
|
||||
|
||||
#: core/models.py:78
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
#: core/models.py:73
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Clé primaire pour l'enregistrement en tant que UUID"
|
||||
|
||||
#: core/models.py:79
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
msgid "created at"
|
||||
msgstr "Créé le"
|
||||
|
||||
#: core/models.py:84
|
||||
msgid "updated at"
|
||||
msgstr ""
|
||||
#: core/models.py:80
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Date et heure de création de l'enregistrement"
|
||||
|
||||
#: core/models.py:85
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
msgid "updated at"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:124
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
#: core/models.py:86
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
|
||||
|
||||
#: core/models.py:125
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:126
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:128
|
||||
#: core/models.py:129
|
||||
msgid "notes"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:130
|
||||
msgid "contact information"
|
||||
msgstr ""
|
||||
msgstr "notes"
|
||||
|
||||
#: core/models.py:131
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr ""
|
||||
msgid "contact information"
|
||||
msgstr "informations de contact"
|
||||
|
||||
#: core/models.py:145
|
||||
msgid "contact"
|
||||
msgstr ""
|
||||
#: core/models.py:132
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr "Un objet JSON contenant les informations de contact"
|
||||
|
||||
#: core/models.py:146
|
||||
msgid "contacts"
|
||||
msgstr ""
|
||||
msgid "contact"
|
||||
msgstr "contact"
|
||||
|
||||
#: core/models.py:220 core/models.py:318 core/models.py:440
|
||||
#: core/models.py:147
|
||||
msgid "contacts"
|
||||
msgstr "contacts"
|
||||
|
||||
#: core/models.py:221 core/models.py:319 core/models.py:441
|
||||
#: mailbox_manager/models.py:24
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
msgstr "nom"
|
||||
|
||||
#: core/models.py:222
|
||||
#: core/models.py:223
|
||||
msgid "audience id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:227
|
||||
msgid "service provider"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:228
|
||||
msgid "service provider"
|
||||
msgstr "fournisseur de service"
|
||||
|
||||
#: core/models.py:229
|
||||
msgid "service providers"
|
||||
msgstr ""
|
||||
msgstr "fournisseurs de service"
|
||||
|
||||
#: core/models.py:326
|
||||
#: core/models.py:327
|
||||
msgid "registration ID list"
|
||||
msgstr ""
|
||||
msgstr "liste d'identifiants d'inscription"
|
||||
|
||||
#: core/models.py:333
|
||||
#: core/models.py:334
|
||||
msgid "domain list"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:349
|
||||
msgid "organization"
|
||||
msgstr ""
|
||||
msgstr "liste de domaines"
|
||||
|
||||
#: core/models.py:350
|
||||
msgid "organizations"
|
||||
msgstr ""
|
||||
msgid "organization"
|
||||
msgstr "organisation"
|
||||
|
||||
#: core/models.py:357
|
||||
#: core/models.py:351
|
||||
msgid "organizations"
|
||||
msgstr "organisations"
|
||||
|
||||
#: core/models.py:358
|
||||
msgid "An organization must have at least a registration ID or a domain."
|
||||
msgstr ""
|
||||
"Une organisation doit avoir au moins un identifiant d'inscription ou un "
|
||||
"domaine."
|
||||
|
||||
#: core/models.py:425
|
||||
#: core/models.py:426
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
"Entrez une sub valide. Cette valeur peut contenir uniquement des lettres, "
|
||||
"des chiffres et des caractères @/./+/-/_"
|
||||
|
||||
#: core/models.py:431
|
||||
#: core/models.py:432
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:433
|
||||
#: core/models.py:434
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Requis. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_ "
|
||||
"uniquement."
|
||||
|
||||
#: core/models.py:439 core/models.py:844
|
||||
#: core/models.py:440 core/models.py:880
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:445
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
msgstr "adresse email"
|
||||
|
||||
#: core/models.py:446
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:447
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:452
|
||||
#: core/models.py:453
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:455
|
||||
#: core/models.py:456
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:457
|
||||
#: core/models.py:458
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:460
|
||||
#: core/models.py:461
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:462
|
||||
#: core/models.py:463
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
msgstr "Si l'utilisateur peut se connecter à cette interface d'administration."
|
||||
|
||||
#: core/models.py:465
|
||||
#: core/models.py:466
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:468
|
||||
#: core/models.py:469
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:487
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
"Si cet utilisateur doit être considéré comme actif. Désélectionnez cette "
|
||||
"option au lieu de supprimer les comptes."
|
||||
|
||||
#: core/models.py:488
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:622
|
||||
msgid "Organization/user relation"
|
||||
msgstr ""
|
||||
#: core/models.py:489
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:623
|
||||
msgid "Organization/user relation"
|
||||
msgstr "Relation entre une organisation et un utilisateur"
|
||||
|
||||
#: core/models.py:624
|
||||
msgid "Organization/user relations"
|
||||
msgstr ""
|
||||
msgstr "Relations entre une organisation et un utilisateur"
|
||||
|
||||
#: core/models.py:628
|
||||
#: core/models.py:629
|
||||
msgid "This user is already in this organization."
|
||||
msgstr ""
|
||||
msgstr "Cet utilisateur est déjà dans cette organisation."
|
||||
|
||||
#: core/models.py:670
|
||||
#: core/models.py:706
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
msgstr "Equipe"
|
||||
|
||||
#: core/models.py:671
|
||||
#: core/models.py:707
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
msgstr "Equipes"
|
||||
|
||||
#: core/models.py:722
|
||||
#: core/models.py:758
|
||||
msgid "Team/user relation"
|
||||
msgstr ""
|
||||
msgstr "Relation entre une équipe et un utilisateur"
|
||||
|
||||
#: core/models.py:723
|
||||
#: core/models.py:759
|
||||
msgid "Team/user relations"
|
||||
msgstr ""
|
||||
msgstr "Relations entre une équipe et un utilisateur"
|
||||
|
||||
#: core/models.py:728
|
||||
#: core/models.py:764
|
||||
msgid "This user is already in this team."
|
||||
msgstr ""
|
||||
msgstr "Cet utilisateur est déjà dans cette équipe."
|
||||
|
||||
#: core/models.py:817
|
||||
#: core/models.py:853
|
||||
msgid "url"
|
||||
msgstr ""
|
||||
msgstr "url"
|
||||
|
||||
#: core/models.py:818
|
||||
#: core/models.py:854
|
||||
msgid "secret"
|
||||
msgstr ""
|
||||
msgstr "secret"
|
||||
|
||||
#: core/models.py:827
|
||||
#: core/models.py:863
|
||||
msgid "Team webhook"
|
||||
msgstr ""
|
||||
msgstr "Webhook d'équipe"
|
||||
|
||||
#: core/models.py:828
|
||||
#: core/models.py:864
|
||||
msgid "Team webhooks"
|
||||
msgstr ""
|
||||
msgstr "Webhooks d'équipe"
|
||||
|
||||
#: core/models.py:861
|
||||
#: core/models.py:897
|
||||
msgid "Team invitation"
|
||||
msgstr ""
|
||||
msgstr "Invitation d'équipe"
|
||||
|
||||
#: core/models.py:862
|
||||
#: core/models.py:898
|
||||
msgid "Team invitations"
|
||||
msgstr ""
|
||||
msgstr "Invitations d'équipe"
|
||||
|
||||
#: core/models.py:887
|
||||
#: core/models.py:923
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur enregistré."
|
||||
|
||||
#: core/models.py:929 core/models.py:935
|
||||
msgid "Invitation to join Desk!"
|
||||
msgstr ""
|
||||
#: core/models.py:965 core/models.py:971
|
||||
msgid "Invitation to join La Régie!"
|
||||
msgstr "Invitation à rejoindre La Régie!"
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr ""
|
||||
msgstr "Logo de l'entreprise"
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr ""
|
||||
msgstr "Bonjour %(name)s"
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
msgstr "Bonjour"
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr ""
|
||||
msgstr "Merci beaucoup pour votre visite!"
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr ""
|
||||
"Cette mail a été envoyée à %(email)s par <a href=\"%(href)s\">%(name)s</a>"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:160
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "La Suite Numérique"
|
||||
msgstr ""
|
||||
msgstr "La Suite Numérique"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:190
|
||||
#: core/templates/mail/text/invitation.txt:5
|
||||
msgid "Invitation to join a team"
|
||||
msgstr ""
|
||||
msgstr "Invitation à rejoindre une équipe"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:198
|
||||
#: core/templates/mail/text/invitation.txt:8
|
||||
msgid "Welcome to"
|
||||
msgstr ""
|
||||
msgstr "Bienvenue sur"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:216
|
||||
#: core/templates/mail/text/invitation.txt:12
|
||||
msgid "Logo"
|
||||
msgstr ""
|
||||
msgstr "Logo"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid ""
|
||||
"We are delighted to welcome you to our community on Régie, your new "
|
||||
"We are delighted to welcome you to our community on La Régie, your new "
|
||||
"companion to simplify the management of your groups efficiently, "
|
||||
"intuitively, and securely."
|
||||
msgstr ""
|
||||
"Nous sommes ravis de vous accueillir dans notre communauté sur Régie, votre "
|
||||
"nouveau compagnon pour simplifier la gestion de vos groupes de manière "
|
||||
"efficace, intuitive et sécurisée."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:231
|
||||
#: core/templates/mail/text/invitation.txt:15
|
||||
@@ -349,21 +377,25 @@ msgid ""
|
||||
"Our application is designed to help you organize, collaborate, and manage "
|
||||
"permissions."
|
||||
msgstr ""
|
||||
"Notre application est conçue pour vous aider à organiser, collaborer et "
|
||||
"gérer les permissions."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:236
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
msgid "With Régie, you will be able to:"
|
||||
msgstr ""
|
||||
msgid "With La Régie, you will be able to:"
|
||||
msgstr "Avec La Régie, vous pourrez :"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:237
|
||||
#: core/templates/mail/text/invitation.txt:17
|
||||
msgid "Create customized groups according to your specific needs."
|
||||
msgstr ""
|
||||
"Créer des groupes personnalisés en fonction de vos besoins spécifiques."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:238
|
||||
#: core/templates/mail/text/invitation.txt:18
|
||||
msgid "Invite members of your team or community in just a few clicks."
|
||||
msgstr ""
|
||||
"Inviter des membres de votre équipe ou de votre communauté en quelques clics."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:239
|
||||
#: core/templates/mail/text/invitation.txt:19
|
||||
@@ -371,11 +403,15 @@ msgid ""
|
||||
"Plan events, meetings, or activities effortlessly with our integrated "
|
||||
"calendar."
|
||||
msgstr ""
|
||||
"Planifier des événements, des réunions ou des activités sans effort avec "
|
||||
"notre calendrier intégré."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:240
|
||||
#: core/templates/mail/text/invitation.txt:20
|
||||
msgid "Share documents, photos, and important information securely."
|
||||
msgstr ""
|
||||
"Partager des documents, des photos et des informations importantes de "
|
||||
"manière sécurisée."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:241
|
||||
#: core/templates/mail/text/invitation.txt:21
|
||||
@@ -383,18 +419,22 @@ msgid ""
|
||||
"Facilitate exchanges and communication with our messaging and group "
|
||||
"discussion tools."
|
||||
msgstr ""
|
||||
"Faciliter les échanges et la communication avec nos outils de messagerie et "
|
||||
"de discussion de groupe."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:252
|
||||
#: core/templates/mail/text/invitation.txt:23
|
||||
msgid "Visit Régie"
|
||||
msgstr ""
|
||||
msgid "Visit La Régie"
|
||||
msgstr "Visiter La Régie"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:261
|
||||
#: core/templates/mail/text/invitation.txt:25
|
||||
msgid ""
|
||||
"We are confident that Régie will help you increase efficiency and "
|
||||
"We are confident that La Régie will help you increase efficiency and "
|
||||
"productivity while strengthening the bond among members."
|
||||
msgstr ""
|
||||
"Nous sommes convaincus que La Régie vous aidera à augmenter l'efficacité et "
|
||||
"la productivité tout en renforçant le lien entre les membres."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:266
|
||||
#: core/templates/mail/text/invitation.txt:26
|
||||
@@ -403,6 +443,10 @@ msgid ""
|
||||
"feedback and suggestions with us. Your feedback is valuable to us and will "
|
||||
"enable us to continually improve our service."
|
||||
msgstr ""
|
||||
"N'hésitez pas à explorer toutes les fonctionnalités de l'application et à "
|
||||
"partager vos commentaires et suggestions avec nous. Vos retours sont "
|
||||
"précieux pour nous et nous permettront de continuer à améliorer notre "
|
||||
"service."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:271
|
||||
#: core/templates/mail/text/invitation.txt:27
|
||||
@@ -410,6 +454,8 @@ msgid ""
|
||||
"Once again, welcome aboard! We are eager to accompany you on this group "
|
||||
"management adventure."
|
||||
msgstr ""
|
||||
"Encore une fois, bienvenue parmi nous ! Nous sommes impatients de vous "
|
||||
"accompagner dans cette aventure de gestion de groupe."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:278
|
||||
#: core/templates/mail/html/new_mailbox.html:272
|
||||
@@ -471,21 +517,21 @@ msgstr "L'équipe de La Suite"
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr ""
|
||||
msgstr "Cette mail a été envoyée à %(email)s par %(name)s [%(href)s]"
|
||||
|
||||
#: mailbox_manager/admin.py:13
|
||||
msgid "Synchronise from dimail"
|
||||
msgstr ""
|
||||
msgstr "Synchroniser à partir de dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:24
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation failed for {domain.name} with message: [{err}]"
|
||||
msgstr ""
|
||||
msgstr "Synchronisation échouée pour {domain.name} avec le message: [{err}]"
|
||||
|
||||
#: mailbox_manager/admin.py:30
|
||||
#, python-brace-format
|
||||
msgid "Synchronisation succeed for {domain.name}. "
|
||||
msgstr ""
|
||||
msgstr "Synchronisation réussie pour {domain.name}. "
|
||||
|
||||
#: mailbox_manager/admin.py:36
|
||||
msgid "Check and update status from dimail"
|
||||
@@ -520,65 +566,69 @@ msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
msgstr "Lecteur"
|
||||
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:31
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr "Actif"
|
||||
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:32
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr "En échec"
|
||||
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:33
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#: mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr "Action requise"
|
||||
|
||||
#: mailbox_manager/models.py:35
|
||||
msgid "Mail domain"
|
||||
msgstr ""
|
||||
msgstr "Domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:36
|
||||
msgid "Mail domains"
|
||||
msgstr ""
|
||||
msgstr "Domaines de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:102
|
||||
msgid "User/mail domain relation"
|
||||
msgstr ""
|
||||
msgstr "Relation entre un utilisateur et un domaine de mail"
|
||||
|
||||
#: mailbox_manager/models.py:103
|
||||
msgid "User/mail domain relations"
|
||||
msgstr ""
|
||||
msgstr "Relations entre un utilisateur et un domaine de mail"
|
||||
|
||||
#: mailbox_manager/models.py:175
|
||||
msgid "local_part"
|
||||
msgstr ""
|
||||
msgstr "local_part"
|
||||
|
||||
#: mailbox_manager/models.py:189
|
||||
msgid "secondary email address"
|
||||
msgstr ""
|
||||
msgstr "adresse email secondaire"
|
||||
|
||||
#: mailbox_manager/models.py:199
|
||||
msgid "Mailbox"
|
||||
msgstr ""
|
||||
msgstr "Boîte mail"
|
||||
|
||||
#: mailbox_manager/models.py:200
|
||||
msgid "Mailboxes"
|
||||
msgstr ""
|
||||
msgstr "Boîtes mails"
|
||||
|
||||
#: mailbox_manager/models.py:224
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr ""
|
||||
"Vous ne pouvez pas créer ou modifier une boîte mail pour un domain désactivé."
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:262
|
||||
#: mailbox_manager/utils/dimail.py:266
|
||||
msgid "Your new mailbox information"
|
||||
msgstr "Informations concernant votre nouvelle boîte mail"
|
||||
|
||||
#: people/settings.py:146
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
msgstr "Anglais"
|
||||
|
||||
#: people/settings.py:147
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
msgstr "Français"
|
||||
|
||||
@@ -12,10 +12,17 @@ from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
@admin.action(description=_("Synchronise from dimail"))
|
||||
def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""Admin action to synchronize existing mailboxes from dimail to our database."""
|
||||
"""Admin action to synchronize existing mailboxes from dimail to our database.
|
||||
Only works on enabled domains."""
|
||||
excluded_domains = []
|
||||
|
||||
client = DimailAPIClient()
|
||||
|
||||
for domain in queryset:
|
||||
if domain.status != enums.MailDomainStatusChoices.ENABLED:
|
||||
excluded_domains.append(domain.name)
|
||||
continue
|
||||
|
||||
try:
|
||||
imported_mailboxes = client.import_mailboxes(domain)
|
||||
except exceptions.HTTPError as err:
|
||||
@@ -31,6 +38,13 @@ def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disabl
|
||||
f"Imported mailboxes: {', '.join(imported_mailboxes)}"
|
||||
),
|
||||
)
|
||||
if excluded_domains:
|
||||
messages.warning(
|
||||
request,
|
||||
_(
|
||||
f"Sync require enabled domains. Excluded domains: {', '.join(excluded_domains)}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description=_("Check and update status from dimail"))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Client serializers for People's mailbox manager app."""
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
@@ -36,23 +35,17 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
Override create function to fire a request on mailbox creation.
|
||||
"""
|
||||
mailbox = super().create(validated_data)
|
||||
if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED:
|
||||
if mailbox.domain.status == enums.MailDomainStatusChoices.ENABLED:
|
||||
client = DimailAPIClient()
|
||||
# send new mailbox request to dimail
|
||||
response = client.create_mailbox(
|
||||
validated_data, self.context["request"].user.sub
|
||||
)
|
||||
response = client.create_mailbox(mailbox, self.context["request"].user.sub)
|
||||
|
||||
# fix format to have actual json, and remove uuid
|
||||
mailbox_data = json.loads(
|
||||
response.content.decode("utf-8").replace("'", '"')
|
||||
)
|
||||
mailbox.status = enums.MailDomainStatusChoices.ENABLED
|
||||
mailbox.save()
|
||||
|
||||
# send confirmation email
|
||||
client.notify_mailbox_creation(
|
||||
recipient=validated_data["secondary_email"], mailbox_data=mailbox_data
|
||||
recipient=mailbox.secondary_email, mailbox_data=response.json()
|
||||
)
|
||||
|
||||
# actually save mailbox on our database
|
||||
@@ -63,6 +56,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"""Serialize mail domain."""
|
||||
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
count_mailboxes = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.MailDomain
|
||||
@@ -75,6 +69,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"abilities",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"count_mailboxes",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
@@ -83,6 +78,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"abilities",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"count_mailboxes",
|
||||
]
|
||||
|
||||
def get_abilities(self, domain) -> dict:
|
||||
@@ -92,6 +88,10 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
return domain.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def get_count_mailboxes(self, domain) -> int:
|
||||
"""Return count of mailboxes for the domain."""
|
||||
return domain.mailboxes.count()
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Override create function to fire a request to dimail upon domain creation.
|
||||
|
||||
@@ -22,6 +22,7 @@ class MailDomainStatusChoices(models.TextChoices):
|
||||
ENABLED = "enabled", _("Enabled")
|
||||
FAILED = "failed", _("Failed")
|
||||
DISABLED = "disabled", _("Disabled")
|
||||
ACTION_REQUIRED = "action_required", _("Action required")
|
||||
|
||||
|
||||
class MailboxStatusChoices(models.TextChoices):
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Management command to check and update domain status"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
import requests
|
||||
|
||||
from mailbox_manager.enums import MailDomainStatusChoices
|
||||
from mailbox_manager.models import MailDomain
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Management command to check and update domains status from dimail
|
||||
"""
|
||||
|
||||
help = (
|
||||
"This command calls dimail to get and update the status of domains."
|
||||
"All domains without a disabled status will be checked and updated if status"
|
||||
"sent by dimail does not match our status saved in our database."
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Handling of the management command."""
|
||||
|
||||
self.stdout.write("Start fetching domain status from dimail...")
|
||||
client = DimailAPIClient()
|
||||
# do not fetch status of disabled domains
|
||||
domains = MailDomain.objects.exclude(status=MailDomainStatusChoices.DISABLED)
|
||||
for domain in domains:
|
||||
old_status = domain.status
|
||||
try:
|
||||
client.fetch_domain_status(domain)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Fetch failed for {domain.name} with message: '{err}'"
|
||||
)
|
||||
)
|
||||
else:
|
||||
action = "UPDATED" if old_status != domain.status else "CHECKED"
|
||||
domain_name = (
|
||||
f"{domain.name[:40]}..." if len(domain.name) > 40 else domain.name
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
(
|
||||
f"Domain {domain_name}"
|
||||
+ "." * (50 - len(domain_name))
|
||||
+ action
|
||||
)
|
||||
)
|
||||
)
|
||||
self.stdout.write("Done", ending="\n")
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-31 17:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0015_change_mailboxes_status_to_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='mailbox',
|
||||
name='domain',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to='mailbox_manager.maildomain'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-03 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0016_alter_mailbox_domain'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='maildomain',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('pending', 'Pending'), ('enabled', 'Enabled'), ('failed', 'Failed'), ('disabled', 'Disabled'), ('action_required', 'Action required')], default='pending', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -25,7 +25,7 @@ class MailDomain(BaseModel):
|
||||
)
|
||||
slug = models.SlugField(null=False, blank=False, unique=True, max_length=80)
|
||||
status = models.CharField(
|
||||
max_length=10,
|
||||
max_length=20,
|
||||
default=MailDomainStatusChoices.PENDING,
|
||||
choices=MailDomainStatusChoices.choices,
|
||||
)
|
||||
@@ -181,7 +181,7 @@ class Mailbox(BaseModel):
|
||||
domain = models.ForeignKey(
|
||||
MailDomain,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="mail_domain",
|
||||
related_name="mailboxes",
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
@@ -114,6 +114,7 @@ def test_api_mail_domains__create_authenticated():
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
}
|
||||
|
||||
# a new domain with status "pending" is created and authenticated user is the owner
|
||||
@@ -185,6 +186,7 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 0,
|
||||
}
|
||||
|
||||
# a new domain with status "failed" is created and authenticated user is the owner
|
||||
|
||||
@@ -71,6 +71,7 @@ def test_api_mail_domains__retrieve_authenticated_related():
|
||||
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user)
|
||||
factories.MailboxFactory.create_batch(10, domain=domain)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/",
|
||||
@@ -85,4 +86,5 @@ def test_api_mail_domains__retrieve_authenticated_related():
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
"count_mailboxes": 10,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager.api.client import serializers
|
||||
from mailbox_manager.tests.fixtures.dimail import (
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -102,19 +106,15 @@ def test_api_mailboxes__create_roles_success(role):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{mail_domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_values['local_part']}@{mail_domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_values['local_part']}@{mail_domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
@@ -161,19 +161,15 @@ def test_api_mailboxes__create_with_accent_success(role):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{mail_domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_values['local_part']}@{mail_domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_values['local_part']}@{mail_domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
@@ -287,7 +283,7 @@ def test_api_mailboxes__no_dimail_call_if_mailbox_creation_failed():
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -340,19 +336,15 @@ def test_api_mailboxes__same_local_part_on_different_domains():
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_values['local_part']}@{access.domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_values['local_part']}@{access.domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
@@ -485,7 +477,7 @@ def test_api_mailboxes__async_dimail_unauthorized(mock_error):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK, # user is in dimail-api
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -538,19 +530,15 @@ def test_api_mailboxes__domain_owner_or_admin_successful_creation_and_provisioni
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsp = rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_data['local_part']}@{access.domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_data['local_part']}@{access.domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
@@ -657,7 +645,7 @@ def test_api_mailboxes__user_unrelated_to_domain():
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -702,7 +690,7 @@ def test_api_mailboxes__handling_dimail_unexpected_error(caplog):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -757,19 +745,15 @@ def test_api_mailboxes__send_correct_logger_infos(mock_info, mock_error):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_data['local_part']}@{access.domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_data['local_part']}@{access.domain.name}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
@@ -818,19 +802,15 @@ def test_api_mailboxes__sends_new_mailbox_notification(mock_info):
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_data['local_part']}@{access.domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
body=response_mailbox_created(
|
||||
f"{mailbox_data['local_part']}@{access.domain}"
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Test the `fetch_domain_status_from_dimail` management command"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager.tests.fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_OK,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_fetch_domain_status():
|
||||
"""Test fetch domain status from dimail"""
|
||||
domain_enabled1 = factories.MailDomainEnabledFactory()
|
||||
domain_enabled2 = factories.MailDomainEnabledFactory()
|
||||
domain_disabled = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.DISABLED
|
||||
)
|
||||
domain_failed = factories.MailDomainFactory(
|
||||
status=enums.MailDomainStatusChoices.FAILED
|
||||
)
|
||||
|
||||
body_content_ok1 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_ok1["name"] = domain_enabled1.name
|
||||
|
||||
body_content_broken = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_broken["name"] = domain_enabled2.name
|
||||
|
||||
body_content_ok2 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_ok2["name"] = domain_disabled.name
|
||||
|
||||
body_content_ok3 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_ok3["name"] = domain_failed.name
|
||||
for domain, body_content in [
|
||||
(domain_enabled1, body_content_ok1),
|
||||
(domain_enabled2, body_content_broken),
|
||||
(domain_failed, body_content_ok3),
|
||||
]:
|
||||
# mock dimail API
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
output = StringIO()
|
||||
call_command("fetch_domain_status", verbosity=2, stdout=output)
|
||||
domain_enabled1.refresh_from_db()
|
||||
domain_enabled2.refresh_from_db()
|
||||
domain_disabled.refresh_from_db()
|
||||
domain_failed.refresh_from_db()
|
||||
# nothing change for the first domain enable
|
||||
assert domain_enabled1.status == enums.MailDomainStatusChoices.ENABLED
|
||||
# status of the second activated domain has changed to action required
|
||||
assert domain_enabled2.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
# status of the failed domain has changed to enabled
|
||||
assert domain_failed.status == enums.MailDomainStatusChoices.ENABLED
|
||||
# disabled domain was excluded
|
||||
assert domain_disabled.status == enums.MailDomainStatusChoices.DISABLED
|
||||
assert output.getvalue().count("CHECKED") == 1
|
||||
assert output.getvalue().count("UPDATED") == 2
|
||||
+165
-3
@@ -1,6 +1,10 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""Define here some fake data from dimail, useful to mock dimail response"""
|
||||
|
||||
import json
|
||||
|
||||
## DOMAINS
|
||||
|
||||
CHECK_DOMAIN_BROKEN = {
|
||||
"name": "example.fr",
|
||||
"state": "broken",
|
||||
@@ -12,7 +16,11 @@ CHECK_DOMAIN_BROKEN = {
|
||||
"smtp_domain": None,
|
||||
"context_name": "example.fr",
|
||||
"transport": None,
|
||||
"domain_exist": {"ok": True, "internal": False, "errors": []},
|
||||
"domain_exist": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"mx": {
|
||||
"ok": False,
|
||||
"internal": False,
|
||||
@@ -70,8 +78,16 @@ CHECK_DOMAIN_BROKEN = {
|
||||
{"code": "no_dkim", "detail": "Il faut un DKIM record, avec la bonne clef"}
|
||||
],
|
||||
},
|
||||
"postfix": {"ok": True, "internal": True, "errors": []},
|
||||
"ox": {"ok": True, "internal": True, "errors": []},
|
||||
"postfix": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"ox": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"cert": {
|
||||
"ok": False,
|
||||
"internal": True,
|
||||
@@ -81,6 +97,141 @@ CHECK_DOMAIN_BROKEN = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
CHECK_DOMAIN_BROKEN_INTERNAL = {
|
||||
"name": "example.fr",
|
||||
"state": "broken",
|
||||
"valid": False,
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"webmail_domain": None,
|
||||
"imap_domain": None,
|
||||
"smtp_domain": None,
|
||||
"context_name": "example.fr",
|
||||
"transport": None,
|
||||
"domain_exist": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"mx": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"cname_imap": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"cname_smtp": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"cname_webmail": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"spf": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"dkim": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"postfix": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"ox": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"cert": {
|
||||
"ok": False,
|
||||
"internal": True,
|
||||
"errors": [
|
||||
{"code": "no_cert", "detail": "Pas de certificat pour ce domaine (ls)"}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
CHECK_DOMAIN_BROKEN_EXTERNAL = {
|
||||
"name": "example.fr",
|
||||
"state": "broken",
|
||||
"valid": False,
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"webmail_domain": None,
|
||||
"imap_domain": None,
|
||||
"smtp_domain": None,
|
||||
"context_name": "example.fr",
|
||||
"transport": None,
|
||||
"domain_exist": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"mx": {
|
||||
"ok": False,
|
||||
"internal": False,
|
||||
"errors": [
|
||||
{
|
||||
"code": "wrong_mx",
|
||||
"detail": "Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr., or je trouve example-fr.mail.protection.outlook.com.",
|
||||
}
|
||||
],
|
||||
},
|
||||
"cname_imap": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"cname_smtp": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"cname_webmail": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"spf": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"dkim": {
|
||||
"ok": True,
|
||||
"internal": False,
|
||||
"errors": [],
|
||||
},
|
||||
"postfix": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"ox": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
"cert": {
|
||||
"ok": True,
|
||||
"internal": True,
|
||||
"errors": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
CHECK_DOMAIN_OK = {
|
||||
"name": "example.fr",
|
||||
"state": "ok",
|
||||
@@ -103,3 +254,14 @@ CHECK_DOMAIN_OK = {
|
||||
"ox": {"ok": True, "internal": True, "errors": []},
|
||||
"cert": {"ok": True, "internal": True, "errors": []},
|
||||
}
|
||||
|
||||
## TOKEN
|
||||
|
||||
TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
|
||||
|
||||
## MAILBOXES
|
||||
|
||||
|
||||
def response_mailbox_created(email_address):
|
||||
"""mimic dimail response upon succesfull mailbox creation."""
|
||||
return json.dumps({"email": email_address, "password": "password"})
|
||||
|
||||
@@ -9,16 +9,55 @@ from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
from .fixtures.dimail import CHECK_DOMAIN_BROKEN, CHECK_DOMAIN_OK
|
||||
from .fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_OK,
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.FAILED,
|
||||
enums.MailDomainStatusChoices.DISABLED,
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_admin_action__fetch_domain_status_from_dimail(client):
|
||||
def test_sync_mailboxes__should_not_sync_if_domain_is_not_enabled(
|
||||
domain_status, client
|
||||
):
|
||||
"""Mailboxes should not be sync'ed on non-enabled domains."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
data = {
|
||||
"action": "sync_mailboxes_from_dimail",
|
||||
"_selected_action": [domain.id],
|
||||
}
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
|
||||
with responses.RequestsMock():
|
||||
# No call expected
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
f"Sync require enabled domains. Excluded domains: {domain}"
|
||||
in response.content.decode("utf-8")
|
||||
)
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_domain_status__should_switch_to_failed_when_domain_broken(client):
|
||||
"""Test admin action to check health of some domains"""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
@@ -32,48 +71,82 @@ def test_admin_action__fetch_domain_status_from_dimail(client):
|
||||
],
|
||||
}
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
body_content_domain1 = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_domain1["name"] = domain1.name
|
||||
body_content_domain2 = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_domain2["name"] = domain2.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain1.name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain2.name}/check/"),
|
||||
body=json.dumps(body_content_domain2),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
domain1.refresh_from_db()
|
||||
domain2.refresh_from_db()
|
||||
assert domain1.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
assert domain2.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
assert "Check domains done with success" in response.content.decode("utf-8")
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
body_content_domain1 = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_domain1["name"] = domain1.name
|
||||
body_content_domain2 = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content_domain2["name"] = domain2.name
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain1.name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain2.name}/check/"),
|
||||
body=json.dumps(body_content_domain2),
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == 200
|
||||
domain1.refresh_from_db()
|
||||
domain2.refresh_from_db()
|
||||
assert domain1.status == enums.MailDomainStatusChoices.FAILED
|
||||
assert domain2.status == enums.MailDomainStatusChoices.FAILED
|
||||
assert "Check domains done with success" in response.content.decode("utf-8")
|
||||
|
||||
# check with a valid domain info from dimail
|
||||
body_content_domain1 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_domain1["name"] = domain1.name
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain1.name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == 200
|
||||
domain1.refresh_from_db()
|
||||
domain2.refresh_from_db()
|
||||
assert domain1.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain2.status == enums.MailDomainStatusChoices.FAILED
|
||||
assert "Check domains done with success" in response.content.decode("utf-8")
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client):
|
||||
"""Test admin action should switch domain state to ENABLED
|
||||
when dimail's response is "ok". It should also activate any pending mailbox."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
domain1 = factories.MailDomainFactory()
|
||||
factories.MailboxFactory.create_batch(3, domain=domain1)
|
||||
|
||||
domain2 = factories.MailDomainFactory()
|
||||
data = {
|
||||
"action": "fetch_domain_status_from_dimail",
|
||||
"_selected_action": [domain1.id],
|
||||
}
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
|
||||
body_content_domain1 = CHECK_DOMAIN_OK.copy()
|
||||
body_content_domain1["name"] = domain1.name
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain1.name}/check/"),
|
||||
body=json.dumps(body_content_domain1),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# we need to get a token to create mailboxes
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(rf".*/domains/{domain1.name}/mailboxes/"),
|
||||
body=response_mailbox_created(f"truc@{domain1.name}"),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(url, data, follow=True)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
domain1.refresh_from_db()
|
||||
domain2.refresh_from_db()
|
||||
assert domain1.status == enums.MailDomainStatusChoices.ENABLED
|
||||
assert domain2.status == enums.MailDomainStatusChoices.PENDING
|
||||
assert "Check domains done with success" in response.content.decode("utf-8")
|
||||
for mailbox in models.Mailbox.objects.filter(domain=domain1):
|
||||
assert mailbox.status == enums.MailboxStatusChoices.ENABLED
|
||||
|
||||
@@ -3,6 +3,7 @@ Unit tests for dimail client
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from email.errors import HeaderParseError, NonASCIILocalPartDefect
|
||||
from logging import Logger
|
||||
@@ -15,7 +16,14 @@ from rest_framework import status
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
from .fixtures.dimail import CHECK_DOMAIN_BROKEN, CHECK_DOMAIN_OK
|
||||
from .fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_BROKEN_EXTERNAL,
|
||||
CHECK_DOMAIN_BROKEN_INTERNAL,
|
||||
CHECK_DOMAIN_OK,
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -156,37 +164,240 @@ def test_dimail_synchronization__synchronize_mailboxes(mock_warning):
|
||||
|
||||
mailbox = models.Mailbox.objects.get()
|
||||
assert mailbox.local_part == "oxadmin"
|
||||
assert mailbox.status == enums.MailboxStatusChoices.ENABLED
|
||||
assert imported_mailboxes == [mailbox_valid["email"]]
|
||||
|
||||
|
||||
def test_dimail__fetch_domain_status_from_dimail():
|
||||
"""Request to dimail health status of a domain"""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
with responses.RequestsMock() as rsps:
|
||||
body_content = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content["name"] = domain.name
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client = DimailAPIClient()
|
||||
response = dimail_client.fetch_domain_status(domain)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert domain.status == enums.MailDomainStatusChoices.FAILED
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
enums.MailDomainStatusChoices.FAILED,
|
||||
enums.MailDomainStatusChoices.ENABLED,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_dimail__fetch_domain_status__switch_to_enabled(domain_status):
|
||||
"""Domains should be enabled when dimail check returns ok status"""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client = DimailAPIClient()
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
|
||||
# Now domain is ok again
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
# call again, should be ok
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.ENABLED,
|
||||
enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
enums.MailDomainStatusChoices.FAILED,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_dimail__fetch_domain_status__switch_to_action_required(
|
||||
domain_status,
|
||||
):
|
||||
"""Domains should be in status action required when dimail check
|
||||
returns broken status for external checks."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
body_content = CHECK_DOMAIN_BROKEN_EXTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client = DimailAPIClient()
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
|
||||
# Support team fixes their part of the problem
|
||||
# Now domain is OK again
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.ENABLED,
|
||||
enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_dimail__fetch_domain_status__switch_to_failed(domain_status):
|
||||
"""Domains should be in status failed when dimail check returns broken status
|
||||
for only internal checks dispite a fix call."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
# nothing can be done by support team, domain should be in failed
|
||||
body_content = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# the endpoint fix is called and still returns KO for internal checks
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client = DimailAPIClient()
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.FAILED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"domain_status",
|
||||
[
|
||||
enums.MailDomainStatusChoices.PENDING,
|
||||
enums.MailDomainStatusChoices.ENABLED,
|
||||
enums.MailDomainStatusChoices.ACTION_REQUIRED,
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
"""Domains should be enabled when dimail check returns ok status
|
||||
after a fix call."""
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
# with all checks KO, domain should be in action required
|
||||
body_content = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client = DimailAPIClient()
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
|
||||
# We assume that the support has fixed their part.
|
||||
# So now dimail returns OK for external checks but still KO for internal checks.
|
||||
# A call to dimail fix endpoint is necessary and will be done by
|
||||
# the fetch_domain_status call
|
||||
body_content = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# the endpoint fix is called and returns OK. Hooray!
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
dimail_client.fetch_domain_status(domain)
|
||||
domain.refresh_from_db()
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
|
||||
|
||||
def test_dimail__enable_pending_mailboxes(caplog):
|
||||
"""Status of pending mailboxes should switch to "enabled"
|
||||
when calling enable_pending_mailboxes."""
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
domain = factories.MailDomainFactory()
|
||||
mailbox1 = factories.MailboxFactory(
|
||||
domain=domain, status=enums.MailboxStatusChoices.PENDING
|
||||
)
|
||||
mailbox2 = factories.MailboxFactory(
|
||||
domain=domain, status=enums.MailboxStatusChoices.PENDING
|
||||
)
|
||||
factories.MailboxFactory.create_batch(
|
||||
2, domain=domain, status=enums.MailboxStatusChoices.ENABLED
|
||||
)
|
||||
|
||||
dimail_client = DimailAPIClient()
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
body=json.dumps(body_content),
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = dimail_client.fetch_domain_status(domain)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert domain.status == enums.MailDomainStatusChoices.ENABLED
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=response_mailbox_created(f"mock@{domain.name}"),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client.enable_pending_mailboxes(domain=domain)
|
||||
|
||||
mailbox1.refresh_from_db()
|
||||
mailbox2.refresh_from_db()
|
||||
assert mailbox1.status == enums.MailboxStatusChoices.ENABLED
|
||||
assert mailbox2.status == enums.MailboxStatusChoices.ENABLED
|
||||
|
||||
assert len(caplog.records) == 6
|
||||
assert (
|
||||
caplog.records[0].message
|
||||
== "Token succesfully granted by mail-provisioning API."
|
||||
)
|
||||
assert (
|
||||
caplog.records[1].message
|
||||
== f"Mailbox successfully created on domain {domain.name} by user None"
|
||||
)
|
||||
assert (
|
||||
caplog.records[2].message
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox1.secondary_email}."
|
||||
)
|
||||
assert (
|
||||
caplog.records[4].message
|
||||
== f"Mailbox successfully created on domain {domain.name} by user None"
|
||||
)
|
||||
assert (
|
||||
caplog.records[5].message
|
||||
== f"Information for mailbox mock@{domain.name} sent to {mailbox2.secondary_email}."
|
||||
)
|
||||
|
||||
@@ -116,15 +116,15 @@ class DimailAPIClient:
|
||||
"""Send a CREATE mailbox request to mail provisioning API."""
|
||||
|
||||
payload = {
|
||||
"givenName": mailbox["first_name"],
|
||||
"surName": mailbox["last_name"],
|
||||
"displayName": f"{mailbox['first_name']} {mailbox['last_name']}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
}
|
||||
headers = self.get_headers(user_sub)
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
f"{self.API_URL}/domains/{mailbox['domain']}/mailboxes/{mailbox['local_part']}",
|
||||
f"{self.API_URL}/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
verify=True,
|
||||
@@ -141,7 +141,7 @@ class DimailAPIClient:
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
logger.info(
|
||||
"Mailbox successfully created on domain %s by user %s",
|
||||
str(mailbox["domain"]),
|
||||
str(mailbox.domain),
|
||||
user_sub,
|
||||
)
|
||||
return response
|
||||
@@ -149,7 +149,7 @@ class DimailAPIClient:
|
||||
if response.status_code == status.HTTP_403_FORBIDDEN:
|
||||
logger.error(
|
||||
"[DIMAIL] 403 Forbidden: you cannot access domain %s",
|
||||
str(mailbox["domain"]),
|
||||
str(mailbox.domain),
|
||||
)
|
||||
raise exceptions.PermissionDenied(
|
||||
"Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
|
||||
@@ -337,10 +337,10 @@ class DimailAPIClient:
|
||||
last_name=dimail_mailbox["surName"],
|
||||
local_part=address.username,
|
||||
domain=domain,
|
||||
secondary_email=dimail_mailbox[
|
||||
"email"
|
||||
], # secondary email is mandatory. Unfortunately, dimail doesn't
|
||||
secondary_email=dimail_mailbox["email"],
|
||||
# secondary email is mandatory. Unfortunately, dimail doesn't
|
||||
# store any. We temporarily give current email as secondary email.
|
||||
status=enums.MailboxStatusChoices.ENABLED,
|
||||
)
|
||||
imported_mailboxes.append(str(mailbox))
|
||||
else:
|
||||
@@ -399,8 +399,25 @@ class DimailAPIClient:
|
||||
return response
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def fetch_domain_status(self, domain):
|
||||
"""Send a request to check domain and update status of our domain."""
|
||||
def enable_pending_mailboxes(self, domain):
|
||||
"""Send requests for all pending mailboxes of a domain."""
|
||||
|
||||
for mailbox in domain.mailboxes.filter(
|
||||
status=enums.MailboxStatusChoices.PENDING
|
||||
):
|
||||
response = self.create_mailbox(mailbox)
|
||||
|
||||
mailbox.status = enums.MailDomainStatusChoices.ENABLED
|
||||
mailbox.save()
|
||||
|
||||
# send confirmation email
|
||||
self.notify_mailbox_creation(
|
||||
recipient=mailbox.secondary_email,
|
||||
mailbox_data=response.json(),
|
||||
)
|
||||
|
||||
def check_domain(self, domain):
|
||||
"""Send a request to dimail to check domain health."""
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/check/",
|
||||
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
|
||||
@@ -408,18 +425,72 @@ class DimailAPIClient:
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
dimail_status = response.json()["state"]
|
||||
if (
|
||||
domain.status != enums.MailDomainStatusChoices.ENABLED
|
||||
and dimail_status == "ok"
|
||||
):
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.save()
|
||||
elif (
|
||||
domain.status != enums.MailDomainStatusChoices.FAILED
|
||||
and dimail_status == "broken"
|
||||
):
|
||||
domain.status = enums.MailDomainStatusChoices.FAILED
|
||||
domain.save()
|
||||
return response
|
||||
return response.json()
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def fix_domain(self, domain):
|
||||
"""Send a request to dimail to fix a domain.
|
||||
Allow to fix internal checks."""
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/fix/",
|
||||
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
|
||||
verify=True,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
logger.info(
|
||||
"Domain %s successfully fixed by dimail",
|
||||
str(domain),
|
||||
)
|
||||
return response.json()
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def fetch_domain_status(self, domain):
|
||||
"""Send a request to check and update status of a domain."""
|
||||
dimail_response = self.check_domain(domain)
|
||||
dimail_state = dimail_response["state"]
|
||||
# if domain is not enabled and dimail returns ok status, enable it
|
||||
if (
|
||||
domain.status != enums.MailDomainStatusChoices.ENABLED
|
||||
and dimail_state == "ok"
|
||||
):
|
||||
self.enable_pending_mailboxes(domain)
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.save()
|
||||
# if dimail returns broken status, we need to extract external and internal checks
|
||||
# and manage the case where the domain has to be fixed by support
|
||||
elif dimail_state == "broken":
|
||||
external_checks = self._get_dimail_checks(dimail_response, internal=False)
|
||||
internal_checks = self._get_dimail_checks(dimail_response, internal=True)
|
||||
# manage the case where the domain has to be fixed by support
|
||||
if not all(external_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ACTION_REQUIRED
|
||||
domain.save()
|
||||
# if all external checks are ok but not internal checks, we need to fix internal checks
|
||||
elif all(external_checks.values()) and not all(internal_checks.values()):
|
||||
# a call to fix endpoint is needed because all external checks are ok
|
||||
dimail_response = self.fix_domain(domain)
|
||||
# we need to check again if all internal and external checks are ok
|
||||
external_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=False
|
||||
)
|
||||
internal_checks = self._get_dimail_checks(
|
||||
dimail_response, internal=True
|
||||
)
|
||||
if all(external_checks.values()) and all(internal_checks.values()):
|
||||
domain.status = enums.MailDomainStatusChoices.ENABLED
|
||||
domain.save()
|
||||
elif all(external_checks.values()) and not all(
|
||||
internal_checks.values()
|
||||
):
|
||||
domain.status = enums.MailDomainStatusChoices.FAILED
|
||||
domain.save()
|
||||
return dimail_response
|
||||
|
||||
def _get_dimail_checks(self, dimail_response, internal: bool):
|
||||
checks = {
|
||||
key: value
|
||||
for key, value in dimail_response.items()
|
||||
if isinstance(value, dict) and value.get("internal") is internal
|
||||
}
|
||||
return {key: value.get("ok", False) for key, value in checks.items()}
|
||||
|
||||
@@ -24,27 +24,22 @@ class NameFromSiretOrganizationPlugin(BaseOrganizationPlugin):
|
||||
_api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
|
||||
|
||||
@staticmethod
|
||||
def _extract_name_from_organization_data(organization_data):
|
||||
"""Extract the name from the organization data."""
|
||||
try:
|
||||
return organization_data["liste_enseignes"][0].title()
|
||||
except KeyError:
|
||||
logger.warning("Missing key 'liste_enseignes' in %s", organization_data)
|
||||
except IndexError:
|
||||
logger.warning("Empty list 'liste_enseignes' in %s", organization_data)
|
||||
return None
|
||||
|
||||
def get_organization_name_from_results(self, data, siret):
|
||||
def get_organization_name_from_results(data, siret):
|
||||
"""Return the organization name from the results of a SIRET search."""
|
||||
for result in data["results"]:
|
||||
nature = "nature_juridique"
|
||||
commune = nature in result and result[nature] == "7210"
|
||||
is_commune = (
|
||||
result.get("nature_juridique") == "7210"
|
||||
) # INSEE code for commune
|
||||
for organization in result["matching_etablissements"]:
|
||||
if organization.get("siret") == siret:
|
||||
if commune:
|
||||
if is_commune:
|
||||
return organization["libelle_commune"].title()
|
||||
|
||||
return self._extract_name_from_organization_data(organization)
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
if store_signs:
|
||||
return store_signs[0].title()
|
||||
if name := result.get("nom_raison_sociale"):
|
||||
return name.title()
|
||||
|
||||
logger.warning("No organization name found for SIRET %s", siret)
|
||||
return None
|
||||
|
||||
@@ -168,3 +168,43 @@ def test_extract_name_from_org_data_when_commune(
|
||||
plugin = NameFromSiretOrganizationPlugin()
|
||||
name = plugin.get_organization_name_from_results(data, "21580304000017")
|
||||
assert name == "Varzy"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_organization_plugins_run_after_create_no_list_enseignes(
|
||||
organization_plugins_settings,
|
||||
):
|
||||
"""Test the run_after_create method of the organization plugins for nominal case."""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://recherche-entreprises.api.gouv.fr/search?q=12345678901234",
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"nom_raison_sociale": "AMAZING ORGANIZATION",
|
||||
# skipping some fields
|
||||
"matching_etablissements": [
|
||||
# skipping some fields
|
||||
{
|
||||
"liste_enseignes": None,
|
||||
"siret": "12345678901234",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"total_results": 1,
|
||||
"page": 1,
|
||||
"per_page": 10,
|
||||
"total_pages": 1,
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
organization = Organization.objects.create(
|
||||
name="12345678901234", registration_id_list=["12345678901234"]
|
||||
)
|
||||
assert organization.name == "Amazing Organization"
|
||||
|
||||
# Check that the organization has been updated in the database also
|
||||
organization.refresh_from_db()
|
||||
assert organization.name == "Amazing Organization"
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "people"
|
||||
version = "1.10.0"
|
||||
version = "1.10.1"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.36.2",
|
||||
"boto3==1.36.11",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
"django-configurations==2.5.1",
|
||||
@@ -42,13 +42,13 @@ dependencies = [
|
||||
"drf_spectacular==0.28.0",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10",
|
||||
"factory_boy==3.3.1",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.23.0",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.4",
|
||||
"PyJWT==2.10.1",
|
||||
"joserfc==1.0.1",
|
||||
"joserfc==1.0.2",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk[django]==2.20.0",
|
||||
"whitenoise==6.8.2",
|
||||
@@ -64,20 +64,20 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==3.2.3",
|
||||
"drf-spectacular-sidecar==2024.12.1",
|
||||
"drf-spectacular-sidecar==2025.2.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==8.31.0",
|
||||
"ipython==8.32.0",
|
||||
"jq==1.8.0",
|
||||
"pyfakefs==5.7.4",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.3",
|
||||
"pylint==3.3.4",
|
||||
"pytest-cov==6.0.0",
|
||||
"pytest-django==4.9.0",
|
||||
"pytest==8.3.4",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.6",
|
||||
"ruff==0.9.2",
|
||||
"ruff==0.9.4",
|
||||
"types-requests==2.32.0.20241016",
|
||||
"freezegun==1.5.1",
|
||||
]
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hookform/resolvers": "3.10.0",
|
||||
"@openfun/cunningham-react": "3.0.0",
|
||||
"@tanstack/react-query": "5.64.2",
|
||||
"i18next": "24.2.1",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"i18next": "24.2.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-i18next": "15.4.0",
|
||||
"react-select": "5.9.0",
|
||||
"react-select": "5.10.0",
|
||||
"sass": "1.83.4",
|
||||
"styled-components": "6.1.14",
|
||||
"zod": "3.24.1",
|
||||
@@ -36,16 +36,16 @@
|
||||
"devDependencies": {
|
||||
"@hookform/devtools": "4.3.3",
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.64.2",
|
||||
"@tanstack/react-query-devtools": "5.66.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
"@testing-library/user-event": "14.6.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.14",
|
||||
"@types/lodash": "4.17.15",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "19.0.7",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "*",
|
||||
"dotenv": "16.4.7",
|
||||
"eslint-config-people": "*",
|
||||
@@ -54,9 +54,9 @@
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.4.2",
|
||||
"stylelint": "16.13.2",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-standard": "37.0.0",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
"stylelint-prettier": "5.0.3",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -110,6 +110,10 @@ describe('ModalAddMailDomain', () => {
|
||||
|
||||
await user.click(buttonSubmit);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.calls().length).toBe(1);
|
||||
});
|
||||
|
||||
expect(fetchMock.lastUrl()).toContain('/mail-domains/');
|
||||
expect(fetchMock.lastOptions()).toEqual({
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { Panel } from '../components/Panel';
|
||||
@@ -23,6 +24,19 @@ describe('PanelTeams', () => {
|
||||
});
|
||||
|
||||
it('renders with no team to display', async () => {
|
||||
useAuthStore.setState({
|
||||
userData: {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
abilities: {
|
||||
teams: { can_view: true, can_create: true },
|
||||
mailboxes: { can_view: true },
|
||||
contacts: { can_view: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fetchMock.mock(`end:/teams/?ordering=-created_at`, []);
|
||||
|
||||
render(<TeamList />, { wrapper: AppWrapper });
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import IconAdd from '@/assets/icons/icon-add.svg';
|
||||
import IconSort from '@/assets/icons/icon-sort.svg';
|
||||
import { Box, BoxButton, StyledLink } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { TeamsOrdering } from '@/features/teams/team-management/api';
|
||||
|
||||
@@ -13,6 +14,9 @@ export const PanelActions = () => {
|
||||
const { t } = useTranslation();
|
||||
const { changeOrdering, ordering } = useTeamStore();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { userData } = useAuthStore();
|
||||
|
||||
const can_create = userData?.abilities?.teams.can_create ?? false;
|
||||
|
||||
const isSortAsc = ordering === TeamsOrdering.BY_CREATED_ON;
|
||||
|
||||
@@ -43,17 +47,19 @@ export const PanelActions = () => {
|
||||
>
|
||||
<IconSort width={30} height={30} aria-hidden="true" />
|
||||
</BoxButton>
|
||||
<StyledLink href="/teams/create">
|
||||
<BoxButton
|
||||
as="span"
|
||||
$margin={{ all: 'auto' }}
|
||||
aria-label={t('Add a team')}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<IconAdd width={27} height={27} aria-hidden="true" />
|
||||
</BoxButton>
|
||||
</StyledLink>
|
||||
{can_create && (
|
||||
<StyledLink href="/teams/create">
|
||||
<BoxButton
|
||||
as="span"
|
||||
$margin={{ all: 'auto' }}
|
||||
aria-label={t('Add a team')}
|
||||
$color={colorsTokens()['primary-600']}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<IconAdd width={27} height={27} aria-hidden="true" />
|
||||
</BoxButton>
|
||||
</StyledLink>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
import { Team, useTeams } from '@/features/teams/team-management';
|
||||
|
||||
import { useTeamStore } from '../store';
|
||||
@@ -17,6 +18,9 @@ interface PanelTeamsStateProps {
|
||||
|
||||
const TeamListState = ({ isLoading, isError, teams }: PanelTeamsStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { userData } = useAuthStore();
|
||||
|
||||
const can_create = userData?.abilities?.teams.can_create ?? false;
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
@@ -42,11 +46,13 @@ const TeamListState = ({ isLoading, isError, teams }: PanelTeamsStateProps) => {
|
||||
<Text as="p" $margin={{ vertical: 'none' }}>
|
||||
{t('0 group to display.')}
|
||||
</Text>
|
||||
<Text as="p">
|
||||
{t(
|
||||
'Create your first team by clicking on the "Create a new team" button.',
|
||||
)}
|
||||
</Text>
|
||||
{can_create && (
|
||||
<Text as="p">
|
||||
{t(
|
||||
'Create your first team by clicking on the "Create a new team" button.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,9 +39,18 @@ test.describe('Config', () => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await expect(page.locator('menu')).toBeHidden();
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
.getByLabel(`Mail Domains button`)
|
||||
.click();
|
||||
await expect(page).toHaveURL(/mail-domains\//);
|
||||
|
||||
await expect(page.getByText('Mail Domains')).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('Mail domains panel', { exact: true })
|
||||
.getByText('Mail Domains'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks that the user abilities display teams', async ({
|
||||
@@ -60,12 +69,6 @@ test.describe('Config', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the groups should see groups
|
||||
// It's now the backend that decides if the user can see the group menu and they
|
||||
// should be redirected to the groups page in such case
|
||||
await keyCloakSignIn(page, browserName, 'team-administrator');
|
||||
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
@@ -83,6 +86,12 @@ test.describe('Config', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the groups should see groups
|
||||
// It's now the backend that decides if the user can see the group menu and they
|
||||
// should be redirected to the groups page in such case
|
||||
await keyCloakSignIn(page, browserName, 'team-administrator');
|
||||
|
||||
await expect(page.locator('menu')).toBeHidden();
|
||||
|
||||
await expect(page.getByText('Groups')).toBeVisible();
|
||||
|
||||
@@ -25,9 +25,9 @@ const payloadGetTeams = {
|
||||
],
|
||||
};
|
||||
|
||||
const mockApiRequests = (page: Page) => {
|
||||
void page.route('**/teams/?ordering=-created_at', (route) => {
|
||||
void route.fulfill({
|
||||
const mockApiRequests = async (page: Page) => {
|
||||
await page.route('**/teams/?ordering=-created_at', async (route) => {
|
||||
await route.fulfill({
|
||||
json: payloadGetTeams.results,
|
||||
});
|
||||
});
|
||||
@@ -40,11 +40,11 @@ test.describe('Keyboard navigation', () => {
|
||||
}) => {
|
||||
const page = await browser.newPage();
|
||||
|
||||
await mockApiRequests(page);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'team-owner-mail-member');
|
||||
|
||||
void mockApiRequests(page);
|
||||
|
||||
const header = page.locator('header');
|
||||
|
||||
// La Gauffre button is loaded asynchronously, so we wait for it to be visible
|
||||
|
||||
@@ -85,10 +85,13 @@ const mailboxesFixtures = {
|
||||
},
|
||||
};
|
||||
|
||||
const interceptCommonApiRequests = (page: Page, mailDomains?: MailDomain[]) => {
|
||||
const interceptCommonApiRequests = async (
|
||||
page: Page,
|
||||
mailDomains?: MailDomain[],
|
||||
) => {
|
||||
const mailDomainsToUse = mailDomains ?? mailDomainsFixtures;
|
||||
void page.route('**/api/v1.0/mail-domains/?page=*', (route) => {
|
||||
void route.fulfill({
|
||||
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: mailDomainsToUse.length,
|
||||
next: null,
|
||||
@@ -98,28 +101,37 @@ const interceptCommonApiRequests = (page: Page, mailDomains?: MailDomain[]) => {
|
||||
});
|
||||
});
|
||||
|
||||
mailDomainsToUse.forEach((mailDomain) => {
|
||||
void page.route(`**/api/v1.0/mail-domains/${mailDomain.slug}/`, (route) => {
|
||||
void route.fulfill({
|
||||
json: mailDomain,
|
||||
});
|
||||
});
|
||||
await Promise.all(
|
||||
mailDomainsToUse.map(async (mailDomain) => {
|
||||
await page.route(
|
||||
`**/api/v1.0/mail-domains/${mailDomain.slug}/`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: mailDomain,
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
void page.route(
|
||||
`**/api/v1.0/mail-domains/${mailDomain.slug}/mailboxes/?page=1**`,
|
||||
(route) => {
|
||||
void route.fulfill({
|
||||
json: {
|
||||
count: mailboxesFixtures.domainFr.page1.length,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: mailboxesFixtures.domainFr.page1,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
});
|
||||
await Promise.all(
|
||||
mailDomainsToUse.map(async (mailDomain) => {
|
||||
await page.route(
|
||||
`**/api/v1.0/mail-domains/${mailDomain.slug}/mailboxes/?page=1**`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: mailboxesFixtures.domainFr.page1.length,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: mailboxesFixtures.domainFr.page1,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const navigateToMailboxCreationFormForMailDomainFr = async (
|
||||
@@ -131,19 +143,20 @@ const navigateToMailboxCreationFormForMailDomainFr = async (
|
||||
};
|
||||
|
||||
test.describe('Mail domain create mailbox', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the mail domains
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
});
|
||||
|
||||
test('checks create mailbox button is visible or not', async ({ page }) => {
|
||||
test('checks create mailbox button is visible or not', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const domains = [...mailDomainsFixtures];
|
||||
domains[0].status = 'enabled';
|
||||
domains[1].status = 'pending';
|
||||
domains[2].status = 'disabled';
|
||||
domains[3].status = 'failed';
|
||||
void interceptCommonApiRequests(page, domains);
|
||||
await interceptCommonApiRequests(page, domains);
|
||||
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the mail domains
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
@@ -190,6 +203,7 @@ test.describe('Mail domain create mailbox', () => {
|
||||
|
||||
test('checks user can create a mailbox when he has post ability', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const newMailbox = {
|
||||
id: '04433733-c9b7-453a-8122-755ac115bb00',
|
||||
@@ -197,13 +211,13 @@ test.describe('Mail domain create mailbox', () => {
|
||||
secondary_email: 'john.doe-complex2024@mail.com',
|
||||
};
|
||||
|
||||
const interceptRequests = (page: Page) => {
|
||||
void interceptCommonApiRequests(page);
|
||||
const interceptRequests = async (page: Page) => {
|
||||
await interceptCommonApiRequests(page);
|
||||
|
||||
void page.route(
|
||||
await page.route(
|
||||
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
|
||||
(route) => {
|
||||
void route.fulfill({
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: [...mailboxesFixtures.domainFr.page1, newMailbox].length,
|
||||
next: null,
|
||||
@@ -214,15 +228,15 @@ test.describe('Mail domain create mailbox', () => {
|
||||
},
|
||||
);
|
||||
|
||||
void page.route(
|
||||
await page.route(
|
||||
'**/api/v1.0/mail-domains/domainfr/mailboxes/',
|
||||
(route) => {
|
||||
async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
void route.fulfill({
|
||||
await route.fulfill({
|
||||
json: newMailbox,
|
||||
});
|
||||
} else {
|
||||
void route.continue();
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -249,8 +263,12 @@ test.describe('Mail domain create mailbox', () => {
|
||||
}
|
||||
});
|
||||
|
||||
void interceptRequests(page);
|
||||
expect(true).toBeTruthy();
|
||||
await interceptRequests(page);
|
||||
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the mail domains
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await navigateToMailboxCreationFormForMailDomainFr(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
@@ -315,15 +333,16 @@ test.describe('Mail domain create mailbox', () => {
|
||||
|
||||
test('checks user is not allowed to create a mailbox when he is missing post ability', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const localMailDomainsFixtures = [...mailDomainsFixtures];
|
||||
localMailDomainsFixtures[0].abilities.post = false;
|
||||
const localMailDomainDomainFr = localMailDomainsFixtures[0];
|
||||
const localMailboxFixtures = { ...mailboxesFixtures };
|
||||
|
||||
const interceptRequests = (page: Page) => {
|
||||
void page.route('**/api/v1.0/mail-domains/?page=*', (route) => {
|
||||
void route.fulfill({
|
||||
const interceptRequests = async (page: Page) => {
|
||||
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: localMailDomainsFixtures.length,
|
||||
next: null,
|
||||
@@ -333,16 +352,16 @@ test.describe('Mail domain create mailbox', () => {
|
||||
});
|
||||
});
|
||||
|
||||
void page.route('**/api/v1.0/mail-domains/domainfr/', (route) => {
|
||||
void route.fulfill({
|
||||
await page.route('**/api/v1.0/mail-domains/domainfr/', async (route) => {
|
||||
await route.fulfill({
|
||||
json: localMailDomainDomainFr,
|
||||
});
|
||||
});
|
||||
|
||||
void page.route(
|
||||
await page.route(
|
||||
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
|
||||
(route) => {
|
||||
void route.fulfill({
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: localMailboxFixtures.domainFr.page1.length,
|
||||
next: null,
|
||||
@@ -355,7 +374,11 @@ test.describe('Mail domain create mailbox', () => {
|
||||
);
|
||||
};
|
||||
|
||||
void interceptRequests(page);
|
||||
await interceptRequests(page);
|
||||
|
||||
await page.goto('/');
|
||||
// Login with a user who has the visibility on the mail domains
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
|
||||
@@ -142,11 +142,6 @@ test.describe('Mail domain', () => {
|
||||
});
|
||||
|
||||
test.describe('user is administrator or owner', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
});
|
||||
|
||||
test.describe('mail domain is enabled', () => {
|
||||
const mailDomainsFixtures: MailDomain[] = [
|
||||
{
|
||||
@@ -215,9 +210,12 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks if all tabs are visible', async ({ page }) => {
|
||||
test('checks if all tabs are visible', async ({ page, browserName }) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -230,9 +228,13 @@ test.describe('Mail domain', () => {
|
||||
|
||||
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -248,6 +250,7 @@ test.describe('Mail domain', () => {
|
||||
|
||||
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const mailboxesFixtures = {
|
||||
domainFr: {
|
||||
@@ -324,6 +327,9 @@ test.describe('Mail domain', () => {
|
||||
|
||||
await interceptApiCalls();
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -360,9 +366,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await expect(page).toHaveURL(/mail-domains\//);
|
||||
@@ -404,9 +416,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -447,9 +465,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-owner');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -476,11 +500,6 @@ test.describe('Mail domain', () => {
|
||||
});
|
||||
|
||||
test.describe('user is member', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
});
|
||||
|
||||
test.describe('mail domain is enabled', () => {
|
||||
const mailDomainsFixtures: MailDomain[] = [
|
||||
{
|
||||
@@ -551,9 +570,13 @@ test.describe('Mail domain', () => {
|
||||
|
||||
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -569,6 +592,7 @@ test.describe('Mail domain', () => {
|
||||
|
||||
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const mailboxesFixtures = {
|
||||
domainFr: {
|
||||
@@ -645,6 +669,9 @@ test.describe('Mail domain', () => {
|
||||
|
||||
await interceptApiCalls();
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -681,9 +708,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await expect(page).toHaveURL(/mail-domains\//);
|
||||
@@ -725,9 +758,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
@@ -768,9 +807,15 @@ test.describe('Mail domain', () => {
|
||||
},
|
||||
];
|
||||
|
||||
test('checks expected elements are visible', async ({ page }) => {
|
||||
test('checks expected elements are visible', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await interceptCommonApiCalls(page, mailDomainsFixtures);
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName, 'mail-member');
|
||||
|
||||
await clickOnMailDomainsNavButton(page);
|
||||
|
||||
await assertMailDomainUpperElementsAreVisible(page);
|
||||
|
||||
@@ -73,16 +73,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
|
||||
test.describe('Mail domains', () => {
|
||||
test.describe('checks all the elements are visible', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
test('checks the sort button', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
// The user is a team administrator, so they land on the team page
|
||||
// This allows to prevent the redirection to the mail domains page
|
||||
// to make the '/api/v1.0/mail-domains/?page=1&ordering=created_at'
|
||||
// query at login, which will be cached and not called afterward in tests.
|
||||
await keyCloakSignIn(page, browserName, 'team-administrator-mail-member');
|
||||
});
|
||||
|
||||
test('checks the sort button', async ({ page }) => {
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
@@ -128,7 +126,7 @@ test.describe('Mail domains', () => {
|
||||
expect(responseSortDesc.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('when no mail domain exists', async ({ page }) => {
|
||||
test('when no mail domain exists', async ({ page, browserName }) => {
|
||||
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
@@ -140,6 +138,13 @@ test.describe('Mail domains', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
// The user is a team administrator, so they land on the team page
|
||||
// This allows to prevent the redirection to the mail domains page
|
||||
// to make the '/api/v1.0/mail-domains/?page=1&ordering=created_at'
|
||||
// query at login, which will be cached and not called afterward in tests.
|
||||
await keyCloakSignIn(page, browserName, 'team-administrator-mail-member');
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
@@ -152,7 +157,7 @@ test.describe('Mail domains', () => {
|
||||
await expect(page.getByText('No domains exist.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('when 4 mail domains exist', async ({ page }) => {
|
||||
test('when 4 mail domains exist', async ({ page, browserName }) => {
|
||||
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
@@ -164,6 +169,13 @@ test.describe('Mail domains', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
// The user is a team administrator, so they land on the team page
|
||||
// This allows to prevent the redirection to the mail domains page
|
||||
// to make the '/api/v1.0/mail-domains/?page=1&ordering=created_at'
|
||||
// query at login, which will be cached and not called afterward in tests.
|
||||
await keyCloakSignIn(page, browserName, 'team-administrator-mail-member');
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:ui": "yarn test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/node": "*",
|
||||
"eslint-config-people": "*",
|
||||
"typescript": "*"
|
||||
|
||||
@@ -9,7 +9,7 @@ const baseURL = `http://localhost:${PORT}`;
|
||||
*/
|
||||
export default defineConfig({
|
||||
// Timeout per test
|
||||
timeout: 30 * 1000,
|
||||
timeout: 10 * 1000,
|
||||
testDir: './__tests__',
|
||||
outputDir: './test-results',
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.5",
|
||||
"@tanstack/eslint-plugin-query": "5.64.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@tanstack/eslint-plugin-query": "5.66.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "15.1.5",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-playwright": "2.1.0",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-testing-library": "7.1.1"
|
||||
|
||||
+197
-185
@@ -1834,57 +1834,57 @@
|
||||
dependencies:
|
||||
buffer "^6.0.3"
|
||||
|
||||
"@next/env@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.5.tgz#2a87f0be705d40b7bd1bf8c66581edc32dcd2b1f"
|
||||
integrity sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==
|
||||
"@next/env@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.6.tgz#2fa863d8c568a56b1c8328a86e621b8bdd4f2a20"
|
||||
integrity sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==
|
||||
|
||||
"@next/eslint-plugin-next@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.5.tgz#00c2c4313760049e5fde11ba7a3bd7a8a3cf97c1"
|
||||
integrity sha512-3cCrXBybsqe94UxD6DBQCYCCiP9YohBMgZ5IzzPYHmPzj8oqNlhBii5b6o1HDDaRHdz2pVnSsAROCtrczy8O0g==
|
||||
"@next/eslint-plugin-next@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.6.tgz#1d6d0fd21f58a650821bafeb3a205b25276ce5e3"
|
||||
integrity sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/swc-darwin-arm64@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.5.tgz#2b59120c1576b718ed4695508c2a9d4a3248ead7"
|
||||
integrity sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==
|
||||
"@next/swc-darwin-arm64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz#92f99badab6cb41f4c5c11a3feffa574bd6a9276"
|
||||
integrity sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==
|
||||
|
||||
"@next/swc-darwin-x64@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.5.tgz#362fa82504ee496af0e7364d8383585f504a03de"
|
||||
integrity sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==
|
||||
"@next/swc-darwin-x64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz#f56f4f8d5f6cb5d3915912ac95590d387f897da5"
|
||||
integrity sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.5.tgz#34bd054f222f45d7b3f5887cc2be3fe87b69e6a5"
|
||||
integrity sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==
|
||||
"@next/swc-linux-arm64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz#0aaffae519c93d1006419d7b98c34ebfd80ecacd"
|
||||
integrity sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.5.tgz#125356cf3e0a9c90e32383e462d14790a6a40763"
|
||||
integrity sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==
|
||||
"@next/swc-linux-arm64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz#e7398d3d31ca60033f708a718cd6c31edcee2e9a"
|
||||
integrity sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.5.tgz#1dc8bf2374b5f3384370ef97e4b3bbead73b250c"
|
||||
integrity sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==
|
||||
"@next/swc-linux-x64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz#d76c72508f4d79d6016cab0c52640b93e590cffb"
|
||||
integrity sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.5.tgz#45dc68da9e1f22de4ef37d397aeb9cf40746871d"
|
||||
integrity sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==
|
||||
"@next/swc-linux-x64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz#0b8ba80a53e65bf8970ed11ea923001e2512c7cb"
|
||||
integrity sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.5.tgz#ce51388f07b9f0f0e1d4f22dcbba75bcca1952fa"
|
||||
integrity sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==
|
||||
"@next/swc-win32-arm64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz#81b5dbbfdada2c05deef688e799af4a24097b65f"
|
||||
integrity sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.1.5":
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.5.tgz#43135b3d9705ae8e6a6b90c8bdf4dc02b9ffc90c"
|
||||
integrity sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==
|
||||
"@next/swc-win32-x64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz#131993c45ffd124fb4b15258e2f3f9669c143e3c"
|
||||
integrity sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -2043,12 +2043,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
|
||||
"@playwright/test@1.49.1":
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
|
||||
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
|
||||
"@playwright/test@1.50.1":
|
||||
version "1.50.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.50.1.tgz#027d00ca77ec79688764eb765cfe9a688807bf0b"
|
||||
integrity sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==
|
||||
dependencies:
|
||||
playwright "1.49.1"
|
||||
playwright "1.50.1"
|
||||
|
||||
"@react-aria/breadcrumbs@^3.5.19":
|
||||
version "3.5.19"
|
||||
@@ -3347,36 +3347,36 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tanstack/eslint-plugin-query@5.64.2":
|
||||
version "5.64.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.64.2.tgz#8ee82dd0768703371850dcc1723691559f92841b"
|
||||
integrity sha512-Xq7jRYvNtGMHjQEGUZLHgEMNB59hgTlqdmKor6cdJ6CMZ/nwmBGpnlr/dcHden7W7BPCdBVN4PWMZBICWvCNQQ==
|
||||
"@tanstack/eslint-plugin-query@5.66.0":
|
||||
version "5.66.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.66.0.tgz#c4537d62ca8bb94fbd7f0b1323053b03a79b39b0"
|
||||
integrity sha512-CzZhBxicLDuuSJbkZ4nPcuBqWnhLu72Zt9p/7qLQ93BepVnZJV6ZDlBLBuN5eg7YRACwECPLsntnwo1zuhgseQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.18.1"
|
||||
|
||||
"@tanstack/query-core@5.64.2":
|
||||
version "5.64.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.64.2.tgz#be06e7c7966d14ea3e7c82bea1086b463f2f6809"
|
||||
integrity sha512-hdO8SZpWXoADNTWXV9We8CwTkXU88OVWRBcsiFrk7xJQnhm6WRlweDzMD+uH+GnuieTBVSML6xFa17C2cNV8+g==
|
||||
"@tanstack/query-core@5.66.0":
|
||||
version "5.66.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.66.0.tgz#163f670b3b4e3b3cdbff6698ad44b2edfcaed185"
|
||||
integrity sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==
|
||||
|
||||
"@tanstack/query-devtools@5.64.2":
|
||||
version "5.64.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.64.2.tgz#3d8f8abb17815a7302482b67713cb933c79ed6ba"
|
||||
integrity sha512-3DautR5UpVZdk/qNIhioZVF7g8fdQZ1U98sBEEk4Tzz3tihSBNMPgwlP40nzgbPEDBIrn/j/oyyvNBVSo083Vw==
|
||||
"@tanstack/query-devtools@5.65.0":
|
||||
version "5.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz#37da5e911543b4f6d98b9a04369eab0de6044ba1"
|
||||
integrity sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==
|
||||
|
||||
"@tanstack/react-query-devtools@5.64.2":
|
||||
version "5.64.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.2.tgz#ece61dfad8032305aefd3f0eb044ccd8304ffa1b"
|
||||
integrity sha512-+ZjJVnPzc8BUV/Eklu2k9T/IAyAyvwoCHqOaOrk2sbU33LFhM52BpX4eyENXn0bx5LwV3DJZgEQlIzucoemfGQ==
|
||||
"@tanstack/react-query-devtools@5.66.0":
|
||||
version "5.66.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.66.0.tgz#d54ba345cecf259181565699af1f80e0b284e714"
|
||||
integrity sha512-uB57wA2YZaQ2fPcFW0E9O1zAGDGSbRKRx84uMk/86VyU9jWVxvJ3Uzp+zNm+nZJYsuekCIo2opTdgNuvM3cKgA==
|
||||
dependencies:
|
||||
"@tanstack/query-devtools" "5.64.2"
|
||||
"@tanstack/query-devtools" "5.65.0"
|
||||
|
||||
"@tanstack/react-query@5.64.2":
|
||||
version "5.64.2"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.64.2.tgz#199c8a5a8ff92a8565f8cdd378747398347512a2"
|
||||
integrity sha512-3pakNscZNm8KJkxmovvtZ4RaXLyiYYobwleTMvpIGUoKRa8j8VlrQKNl5W8VUEfVfZKkikvXVddLuWMbcSCA1Q==
|
||||
"@tanstack/react-query@5.66.0":
|
||||
version "5.66.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.66.0.tgz#9f7aa1b3e844ea6a0ad2ee61fccaed76e614b865"
|
||||
integrity sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.64.2"
|
||||
"@tanstack/query-core" "5.66.0"
|
||||
|
||||
"@tanstack/react-table@8.20.6":
|
||||
version "8.20.6"
|
||||
@@ -3424,10 +3424,10 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
"@testing-library/user-event@14.6.0":
|
||||
version "14.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.0.tgz#6748ec1ac6df9291e92b6abc0f3530b3842bf34d"
|
||||
integrity sha512-+jsfK7kVJbqnCYtLTln8Ja/NmVrZRwBJHmHR9IxIVccMWSOZ6Oy0FkDJNeyVu4QSpMNmRfy10Xb76ObRDlWWBQ==
|
||||
"@testing-library/user-event@14.6.1":
|
||||
version "14.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149"
|
||||
integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
@@ -3545,10 +3545,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/lodash@4.17.14":
|
||||
version "4.17.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.14.tgz#bafc053533f4cdc5fcc9635af46a963c1f3deaff"
|
||||
integrity sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==
|
||||
"@types/lodash@4.17.15":
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.15.tgz#12d4af0ed17cc7600ce1f9980cec48fc17ad1e89"
|
||||
integrity sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==
|
||||
|
||||
"@types/lodash@^4.14.168":
|
||||
version "4.17.13"
|
||||
@@ -3565,10 +3565,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||
|
||||
"@types/node@*", "@types/node@22.10.7":
|
||||
version "22.10.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7"
|
||||
integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==
|
||||
"@types/node@*", "@types/node@22.13.0":
|
||||
version "22.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.0.tgz#d376dd9a0ee2f9382d86c2d5d7beb4d198b4ea8c"
|
||||
integrity sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
@@ -3609,10 +3609,10 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@19.0.7":
|
||||
version "19.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.7.tgz#c451968b999d1cb2d9207dc5ff56496164cf511d"
|
||||
integrity sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==
|
||||
"@types/react@19.0.8":
|
||||
version "19.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e"
|
||||
integrity sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
@@ -3648,16 +3648,16 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz#b47a398e0e551cb008c60190b804394e6852c863"
|
||||
integrity sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==
|
||||
"@typescript-eslint/eslint-plugin@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz#63a1b0d24d85a971949f8d71d693019f58d2e861"
|
||||
integrity sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.20.0"
|
||||
"@typescript-eslint/type-utils" "8.20.0"
|
||||
"@typescript-eslint/utils" "8.20.0"
|
||||
"@typescript-eslint/visitor-keys" "8.20.0"
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/type-utils" "8.22.0"
|
||||
"@typescript-eslint/utils" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
@@ -3678,15 +3678,15 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/parser@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.20.0.tgz#5caf2230a37094dc0e671cf836b96dd39b587ced"
|
||||
integrity sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==
|
||||
"@typescript-eslint/parser@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.22.0.tgz#f21c5db24271f182ebbb4ba8c7ad3eb76e5f5f3a"
|
||||
integrity sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.20.0"
|
||||
"@typescript-eslint/types" "8.20.0"
|
||||
"@typescript-eslint/typescript-estree" "8.20.0"
|
||||
"@typescript-eslint/visitor-keys" "8.20.0"
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
@@ -3724,13 +3724,13 @@
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz#aaf4198b509fb87a6527c02cfbfaf8901179e75c"
|
||||
integrity sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==
|
||||
"@typescript-eslint/scope-manager@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz#e85836ddeb8eae715f870628bcc32fe96aaf4d0e"
|
||||
integrity sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.20.0"
|
||||
"@typescript-eslint/visitor-keys" "8.20.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.13.0":
|
||||
version "8.13.0"
|
||||
@@ -3742,13 +3742,13 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz#958171d86b213a3f32b5b16b91db267968a4ef19"
|
||||
integrity sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==
|
||||
"@typescript-eslint/type-utils@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz#cd9f23c23f021357ef0baa3490d4d96edcc97509"
|
||||
integrity sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.20.0"
|
||||
"@typescript-eslint/utils" "8.20.0"
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
"@typescript-eslint/utils" "8.22.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
@@ -3767,10 +3767,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c"
|
||||
integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==
|
||||
|
||||
"@typescript-eslint/types@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.20.0.tgz#487de5314b5415dee075e95568b87a75a3e730cf"
|
||||
integrity sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==
|
||||
"@typescript-eslint/types@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.22.0.tgz#d9dec7116479ad03aeb6c8ac9c5223c4c79cf360"
|
||||
integrity sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.13.0":
|
||||
version "8.13.0"
|
||||
@@ -3814,13 +3814,13 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz#658cea07b7e5981f19bce5cf1662cb70ad59f26b"
|
||||
integrity sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==
|
||||
"@typescript-eslint/typescript-estree@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz#c188c3e19529d5b3145577c0bd967e2683b114df"
|
||||
integrity sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.20.0"
|
||||
"@typescript-eslint/visitor-keys" "8.20.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/visitor-keys" "8.22.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -3838,15 +3838,15 @@
|
||||
"@typescript-eslint/types" "8.13.0"
|
||||
"@typescript-eslint/typescript-estree" "8.13.0"
|
||||
|
||||
"@typescript-eslint/utils@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.20.0.tgz#53127ecd314b3b08836b4498b71cdb86f4ef3aa2"
|
||||
integrity sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==
|
||||
"@typescript-eslint/utils@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.22.0.tgz#c8cc4e52a9c711af8a741a82dc5d7242b7a8dd44"
|
||||
integrity sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.20.0"
|
||||
"@typescript-eslint/types" "8.20.0"
|
||||
"@typescript-eslint/typescript-estree" "8.20.0"
|
||||
"@typescript-eslint/scope-manager" "8.22.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
"@typescript-eslint/typescript-estree" "8.22.0"
|
||||
|
||||
"@typescript-eslint/utils@^8.15.0":
|
||||
version "8.16.0"
|
||||
@@ -3892,12 +3892,12 @@
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.20.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz#2df6e24bc69084b81f06aaaa48d198b10d382bed"
|
||||
integrity sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==
|
||||
"@typescript-eslint/visitor-keys@8.22.0":
|
||||
version "8.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz#02cc005014c372033eb9171e2275b76cba722a3f"
|
||||
integrity sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.20.0"
|
||||
"@typescript-eslint/types" "8.22.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -5449,12 +5449,12 @@ escodegen@^2.0.0:
|
||||
optionalDependencies:
|
||||
source-map "~0.6.1"
|
||||
|
||||
eslint-config-next@15.1.5:
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.5.tgz#15b6cdd2469c7bb69af8d832bf6a10224d19c397"
|
||||
integrity sha512-Awm7iUJY8toOR+fU8yTxZnA7/LyOGUGOd6cENCuDfJ3gucHOSmLdOSGJ4u+nlrs8p5qXemua42bZmq+uOzxl6Q==
|
||||
eslint-config-next@15.1.6:
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.1.6.tgz#c056b7325dc70a247895c7c85515ebaae2bab35d"
|
||||
integrity sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==
|
||||
dependencies:
|
||||
"@next/eslint-plugin-next" "15.1.5"
|
||||
"@next/eslint-plugin-next" "15.1.6"
|
||||
"@rushstack/eslint-patch" "^1.10.3"
|
||||
"@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
"@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -5553,10 +5553,10 @@ eslint-plugin-jsx-a11y@6.10.2, eslint-plugin-jsx-a11y@^6.10.0:
|
||||
safe-regex-test "^1.0.3"
|
||||
string.prototype.includes "^2.0.1"
|
||||
|
||||
eslint-plugin-playwright@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-2.1.0.tgz#9d84b5d839226e1f9ac926663baee29d103dcc99"
|
||||
integrity sha512-wMbHOehofSB1cBdzz2CLaCYaKNLeTQ0YnOW+7AHa281TJqlpEJUBgTHbRUYOUxiXphfWwOyTPvgr6vvEmArbSA==
|
||||
eslint-plugin-playwright@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-2.2.0.tgz#d7eda21e670274fc0c006e11ba5cc2c8417b2a6e"
|
||||
integrity sha512-qSQpAw7RcSzE3zPp8FMGkthaCWovHZ/BsXtpmnGax9vQLIovlh1bsZHEa2+j2lv9DWhnyeLM/qZmp7ffQZfQvg==
|
||||
dependencies:
|
||||
globals "^13.23.0"
|
||||
|
||||
@@ -6422,7 +6422,14 @@ i18next-parser@9.1.0:
|
||||
vinyl "^3.0.0"
|
||||
vinyl-fs "^4.0.0"
|
||||
|
||||
i18next@24.2.1, "i18next@^23.5.1 || ^24.2.0":
|
||||
i18next@24.2.2:
|
||||
version "24.2.2"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.2.tgz#3ba3d213302068d569142737f03f30929de696de"
|
||||
integrity sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
|
||||
"i18next@^23.5.1 || ^24.2.0":
|
||||
version "24.2.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.1.tgz#91e8f11fc9bd7042ec0bd36bed2dd0457aaa35fa"
|
||||
integrity sha512-Q2wC1TjWcSikn1VAJg13UGIjc+okpFxQTxjVAymOnSA3RpttBQNMPf2ovcgoFVsV4QNxTfNZMAxorXZXsk4fBA==
|
||||
@@ -6446,7 +6453,7 @@ ignore@^5.2.0, ignore@^5.3.1:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
ignore@^7.0.1:
|
||||
ignore@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.3.tgz#397ef9315dfe0595671eefe8b633fec6943ab733"
|
||||
integrity sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==
|
||||
@@ -7741,17 +7748,22 @@ nanoid@^3.3.6, nanoid@^3.3.7:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
|
||||
nanoid@^3.3.8:
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@15.1.5:
|
||||
version "15.1.5"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.5.tgz#80972a606d17ead86323d3405abf3bd3e2c894c8"
|
||||
integrity sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==
|
||||
next@15.1.6:
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.6.tgz#ce22fd0a8f36da1fc4aba86e3ec7e98eb248c555"
|
||||
integrity sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==
|
||||
dependencies:
|
||||
"@next/env" "15.1.5"
|
||||
"@next/env" "15.1.6"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
@@ -7759,14 +7771,14 @@ next@15.1.5:
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.1.5"
|
||||
"@next/swc-darwin-x64" "15.1.5"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.5"
|
||||
"@next/swc-linux-arm64-musl" "15.1.5"
|
||||
"@next/swc-linux-x64-gnu" "15.1.5"
|
||||
"@next/swc-linux-x64-musl" "15.1.5"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.5"
|
||||
"@next/swc-win32-x64-msvc" "15.1.5"
|
||||
"@next/swc-darwin-arm64" "15.1.6"
|
||||
"@next/swc-darwin-x64" "15.1.6"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.6"
|
||||
"@next/swc-linux-arm64-musl" "15.1.6"
|
||||
"@next/swc-linux-x64-gnu" "15.1.6"
|
||||
"@next/swc-linux-x64-musl" "15.1.6"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.6"
|
||||
"@next/swc-win32-x64-msvc" "15.1.6"
|
||||
sharp "^0.33.5"
|
||||
|
||||
no-case@^3.0.4:
|
||||
@@ -8083,17 +8095,17 @@ pkg-dir@^4.2.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
playwright-core@1.49.1:
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
|
||||
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==
|
||||
playwright-core@1.50.1:
|
||||
version "1.50.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.50.1.tgz#6a0484f1f1c939168f40f0ab3828c4a1592c4504"
|
||||
integrity sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==
|
||||
|
||||
playwright@1.49.1:
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
|
||||
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
|
||||
playwright@1.50.1:
|
||||
version "1.50.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.50.1.tgz#2f93216511d65404f676395bfb97b41aa052b180"
|
||||
integrity sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==
|
||||
dependencies:
|
||||
playwright-core "1.49.1"
|
||||
playwright-core "1.50.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
@@ -8143,12 +8155,12 @@ postcss@8.4.38:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
postcss@^8.4.49:
|
||||
version "8.4.49"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19"
|
||||
integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
|
||||
postcss@^8.5.1:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.1.tgz#e2272a1f8a807fafa413218245630b5db10a3214"
|
||||
integrity sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
nanoid "^3.3.8"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
@@ -8396,10 +8408,10 @@ react-modal@3.16.3:
|
||||
react-lifecycles-compat "^3.0.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-select@5.9.0:
|
||||
version "5.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.9.0.tgz#41325b7bfe8452a8d401b82fc4fb7d44a03e29c5"
|
||||
integrity sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==
|
||||
react-select@5.10.0:
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.0.tgz#9b5f4544cfecdfc744184b87651468ee0fb6e172"
|
||||
integrity sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.0"
|
||||
"@emotion/cache" "^11.4.0"
|
||||
@@ -9216,17 +9228,17 @@ stylelint-config-standard@37.0.0:
|
||||
dependencies:
|
||||
stylelint-config-recommended "^15.0.0"
|
||||
|
||||
stylelint-prettier@5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-prettier/-/stylelint-prettier-5.0.2.tgz#82c0a813d8981fe094fca28126fc88cee22fa789"
|
||||
integrity sha512-qJ+BN+1T2ZcKz9WIrv0x+eFGHzSUnXfXd5gL///T6XoJvr3D8/ztzz2fhtmXef7Vb8P33zBXmLTTveByr0nwBw==
|
||||
stylelint-prettier@5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-prettier/-/stylelint-prettier-5.0.3.tgz#470134ec5ed86501119be9a5cb44709b467d7c91"
|
||||
integrity sha512-B6V0oa35ekRrKZlf+6+jA+i50C4GXJ7X1PPmoCqSUoXN6BrNF6NhqqhanvkLjqw2qgvrS0wjdpeC+Tn06KN3jw==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
stylelint@16.13.2:
|
||||
version "16.13.2"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.13.2.tgz#a0e8aab7cedde55f185ac8af6f526e53df783294"
|
||||
integrity sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==
|
||||
stylelint@16.14.1:
|
||||
version "16.14.1"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.14.1.tgz#32d3df8c5ce7ba0275ce6a4a966b6099d7a828e3"
|
||||
integrity sha512-oqCL7AC3786oTax35T/nuLL8p2C3k/8rHKAooezrPGRvUX0wX+qqs5kMWh5YYT4PHQgVDobHT4tw55WgpYG6Sw==
|
||||
dependencies:
|
||||
"@csstools/css-parser-algorithms" "^3.0.4"
|
||||
"@csstools/css-tokenizer" "^3.0.3"
|
||||
@@ -9246,7 +9258,7 @@ stylelint@16.13.2:
|
||||
globby "^11.1.0"
|
||||
globjoin "^0.1.4"
|
||||
html-tags "^3.3.1"
|
||||
ignore "^7.0.1"
|
||||
ignore "^7.0.3"
|
||||
imurmurhash "^0.1.4"
|
||||
is-plain-object "^5.0.0"
|
||||
known-css-properties "^0.35.0"
|
||||
@@ -9255,7 +9267,7 @@ stylelint@16.13.2:
|
||||
micromatch "^4.0.8"
|
||||
normalize-path "^3.0.0"
|
||||
picocolors "^1.1.1"
|
||||
postcss "^8.4.49"
|
||||
postcss "^8.5.1"
|
||||
postcss-resolve-nested-selector "^0.1.6"
|
||||
postcss-safe-parser "^7.0.1"
|
||||
postcss-selector-parser "^7.0.0"
|
||||
|
||||
@@ -102,3 +102,11 @@ ingressAdmin:
|
||||
enabled: true
|
||||
host: desk.127.0.0.1.nip.io
|
||||
|
||||
mailcatcher:
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
enabled: true
|
||||
host: mailcatcher.127.0.0.1.nip.io
|
||||
|
||||
@@ -107,3 +107,12 @@ ingress:
|
||||
ingressAdmin:
|
||||
enabled: true
|
||||
host: desk.127.0.0.1.nip.io
|
||||
|
||||
mailcatcher:
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
ingress:
|
||||
enabled: true
|
||||
host: mailcatcher.127.0.0.1.nip.io
|
||||
|
||||
@@ -100,3 +100,10 @@ releases:
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.desk.yaml.gotmpl
|
||||
|
||||
- name: mailcatcher
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./mailcatcher
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.desk.yaml.gotmpl
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: mailcatcher
|
||||
description: A simple SMTP server with a web interface for testing email in development
|
||||
version: 1.0.0
|
||||
type: application
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mailcatcher
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
containers:
|
||||
- name: mailcatcher
|
||||
image: "{{ .Values.mailcatcher.image.repository }}:{{ .Values.mailcatcher.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.mailcatcher.image.pullPolicy }}
|
||||
ports:
|
||||
- name: smtp
|
||||
containerPort: 1025
|
||||
- name: http
|
||||
containerPort: 1080
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.mailcatcher.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.mailcatcher.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: mailcatcher
|
||||
port:
|
||||
number: 1080
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mailcatcher
|
||||
labels:
|
||||
app: mailcatcher
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 1025
|
||||
targetPort: smtp
|
||||
protocol: TCP
|
||||
name: smtp
|
||||
- port: 1080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: mailcatcher
|
||||
@@ -0,0 +1,16 @@
|
||||
image:
|
||||
repository: sj26/mailcatcher
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
host: ""
|
||||
@@ -23,10 +23,10 @@
|
||||
<mj-image src="{% base64_static 'images/logo.png' %}" width="157px" align="left" alt="{%trans 'Logo' %}" />
|
||||
|
||||
<!-- Main Message -->
|
||||
<mj-text>{% trans "We are delighted to welcome you to our community on Régie, your new companion to simplify the management of your groups efficiently, intuitively, and securely." %}</mj-text>
|
||||
<mj-text>{% trans "We are delighted to welcome you to our community on La Régie, your new companion to simplify the management of your groups efficiently, intuitively, and securely." %}</mj-text>
|
||||
<mj-text>{% trans "Our application is designed to help you organize, collaborate, and manage permissions." %}</mj-text>
|
||||
<mj-text>
|
||||
{% trans "With Régie, you will be able to:" %}
|
||||
{% trans "With La Régie, you will be able to:" %}
|
||||
<ul>
|
||||
<li>{% trans "Create customized groups according to your specific needs."%}</li>
|
||||
<li>{% trans "Invite members of your team or community in just a few clicks."%}</li>
|
||||
@@ -36,9 +36,9 @@
|
||||
</ul>
|
||||
</mj-text>
|
||||
<mj-button href="//{{site.domain}}" background-color="#000091" color="white" padding-bottom="30px">
|
||||
{% trans "Visit Régie"%}
|
||||
{% trans "Visit La Régie"%}
|
||||
</mj-button>
|
||||
<mj-text>{% trans "We are confident that Régie will help you increase efficiency and productivity while strengthening the bond among members." %}</mj-text>
|
||||
<mj-text>{% trans "We are confident that La Régie will help you increase efficiency and productivity while strengthening the bond among members." %}</mj-text>
|
||||
<mj-text>{% trans "Feel free to explore all the features of the application and share your feedback and suggestions with us. Your feedback is valuable to us and will enable us to continually improve our service." %}</mj-text>
|
||||
<mj-text>{% trans "Once again, welcome aboard! We are eager to accompany you on this group management adventure." %}</mj-text>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user