mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-08-17 21:25:52 +02:00
Use the normal invitation flow when inviting an initial tenant admin.
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { sendTenantAdminInviteEmail } from "../email-service";
|
||||
|
||||
describe("sendTenantAdminInviteEmail", () => {
|
||||
const mockTenant = {
|
||||
id: "test-tenant-id",
|
||||
shortName: "testcorp",
|
||||
longName: "Test Corporation GmbH",
|
||||
descriptions: { en: "A test corporation" },
|
||||
languages: ["en"],
|
||||
defaultLanguage: "en",
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "SETTINGS" as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
logo: null,
|
||||
links: {
|
||||
website: "https://testcorp.com",
|
||||
imprint: "https://testcorp.com/imprint",
|
||||
privacyStatement: "https://testcorp.com/privacy",
|
||||
},
|
||||
};
|
||||
|
||||
it("should accept correct parameters and not throw for German language", () => {
|
||||
const adminEmail = "admin@testcorp.com";
|
||||
const adminName = "Test Admin";
|
||||
const registrationUrl = "http://localhost:5173/register?tenant=test";
|
||||
|
||||
// This test just ensures the function accepts the right parameters
|
||||
// and doesn't throw on setup (actual email sending will fail without SMTP config)
|
||||
expect(() => {
|
||||
sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "de");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should accept correct parameters and not throw for English language", () => {
|
||||
const adminEmail = "admin@testcorp.com";
|
||||
const adminName = "Test Admin";
|
||||
const registrationUrl = "http://localhost:5173/register?tenant=test";
|
||||
|
||||
expect(() => {
|
||||
sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl, "en");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should accept correct parameters and not throw with default language", () => {
|
||||
const adminEmail = "admin@testcorp.com";
|
||||
const adminName = "Test Admin";
|
||||
const registrationUrl = "http://localhost:5173/register?tenant=test";
|
||||
|
||||
expect(() => {
|
||||
sendTenantAdminInviteEmail(adminEmail, adminName, mockTenant, registrationUrl);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -254,37 +254,6 @@ export async function sendConfirmationEmail(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send tenant administrator invitation email
|
||||
* @param {string} adminEmail - Email address of the invited administrator
|
||||
* @param {string} adminName - Name of the invited administrator
|
||||
* @param {SelectTenant} tenant - Tenant information for branding
|
||||
* @param {string} registrationUrl - URL for administrator to register
|
||||
* @param {Language} [language="en"] - Email language
|
||||
* @throws {Error} When email sending fails
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sendTenantAdminInviteEmail(
|
||||
adminEmail: string,
|
||||
adminName: string,
|
||||
tenant: SelectTenant,
|
||||
registrationUrl: string,
|
||||
language: Language = "en",
|
||||
): Promise<void> {
|
||||
const recipient: EmailRecipient = {
|
||||
email: adminEmail,
|
||||
name: adminName,
|
||||
language,
|
||||
};
|
||||
|
||||
const subject =
|
||||
language === "en" ? "Invitation as Tenant Administrator" : "Einladung als Tenant-Administrator";
|
||||
|
||||
await sendTemplatedEmail("tenant-admin-invite", recipient, subject, language, tenant, {
|
||||
registrationUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send user invitation email for existing tenant
|
||||
* @param {string} userEmail - Email address of the invited user
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Einladung als Tenant-Administrator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
.email-container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2563eb;
|
||||
}
|
||||
h1 {
|
||||
color: #1f2937;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #2563eb;
|
||||
color: white !important;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
.tenant-info {
|
||||
background-color: #f9fafb;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #2563eb;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<div class="logo">Open Reception</div>
|
||||
</div>
|
||||
|
||||
<h1>Einladung als Tenant-Administrator</h1>
|
||||
|
||||
<p>Hallo {{recipient.name}},</p>
|
||||
|
||||
<p>Sie wurden als Administrator für einen neuen Tenant in Open Reception eingeladen!</p>
|
||||
|
||||
<div class="tenant-info">
|
||||
<h3>Tenant-Details:</h3>
|
||||
<p><strong>Name:</strong> {{tenant.shortName}}</p>
|
||||
<p><strong>Beschreibung:</strong> {{tenant.longName}}</p>
|
||||
</div>
|
||||
|
||||
<p>Als Tenant-Administrator können Sie:</p>
|
||||
<ul>
|
||||
<li>Die Konfiguration Ihres Tenants verwalten</li>
|
||||
<li>Mitarbeiter einladen und verwalten</li>
|
||||
<li>Kanäle und Services einrichten</li>
|
||||
<li>Termine und Buchungen überwachen</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Registrieren Sie sich jetzt, um loszulegen:</strong></p>
|
||||
|
||||
<p><a href="{{registrationUrl}}" class="button">Jetzt registrieren</a></p>
|
||||
|
||||
<p>
|
||||
Wenn der Button nicht funktioniert, kopieren Sie diesen Link in Ihren Browser:<br />
|
||||
{{registrationUrl}}
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
Diese Einladung ist 7 Tage gültig. Falls Sie diese E-Mail nicht angefordert haben, können
|
||||
Sie sie ignorieren.
|
||||
</p>
|
||||
<p>Open Reception Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
Einladung als Tenant-Administrator
|
||||
|
||||
Hallo {{recipient.name}},
|
||||
|
||||
Sie wurden als Administrator für einen neuen Tenant in Open Reception eingeladen!
|
||||
|
||||
Tenant-Details:
|
||||
- Name: {{tenant.shortName}}
|
||||
- Beschreibung: {{tenant.longName}}
|
||||
|
||||
Als Tenant-Administrator können Sie:
|
||||
- Die Konfiguration Ihres Tenants verwalten
|
||||
- Mitarbeiter einladen und verwalten
|
||||
- Kanäle und Services einrichten
|
||||
- Termine und Buchungen überwachen
|
||||
|
||||
Registrieren Sie sich jetzt, um loszulegen:
|
||||
{{registrationUrl}}
|
||||
|
||||
Diese Einladung ist 7 Tage gültig. Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.
|
||||
|
||||
--
|
||||
Open Reception Team
|
||||
@@ -1,112 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Invitation as Tenant Administrator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
.email-container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2563eb;
|
||||
}
|
||||
h1 {
|
||||
color: #1f2937;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #2563eb;
|
||||
color: white !important;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
.tenant-info {
|
||||
background-color: #f9fafb;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #2563eb;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<div class="logo">Open Reception</div>
|
||||
</div>
|
||||
|
||||
<h1>Invitation as Tenant Administrator</h1>
|
||||
|
||||
<p>Hello {{recipient.name}},</p>
|
||||
|
||||
<p>You have been invited as an administrator for a new tenant in Open Reception!</p>
|
||||
|
||||
<div class="tenant-info">
|
||||
<h3>Tenant Details:</h3>
|
||||
<p><strong>Name:</strong> {{tenant.shortName}}</p>
|
||||
<p><strong>Description:</strong> {{tenant.longName}}</p>
|
||||
</div>
|
||||
|
||||
<p>As a tenant administrator, you can:</p>
|
||||
<ul>
|
||||
<li>Manage your tenant's configuration</li>
|
||||
<li>Invite and manage staff members</li>
|
||||
<li>Set up channels and services</li>
|
||||
<li>Monitor appointments and bookings</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Register now to get started:</strong></p>
|
||||
|
||||
<p><a href="{{registrationUrl}}" class="button">Register Now</a></p>
|
||||
|
||||
<p>
|
||||
If the button doesn't work, copy this link into your browser:<br />
|
||||
{{registrationUrl}}
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
This invitation is valid for 7 days. If you didn't request this email, you can ignore it.
|
||||
</p>
|
||||
<p>Open Reception Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
Invitation as Tenant Administrator
|
||||
|
||||
Hello {{recipient.name}},
|
||||
|
||||
You have been invited as an administrator for a new tenant in Open Reception!
|
||||
|
||||
Tenant Details:
|
||||
- Name: {{tenant.shortName}}
|
||||
- Description: {{tenant.longName}}
|
||||
|
||||
As a tenant administrator, you can:
|
||||
- Manage your tenant's configuration
|
||||
- Invite and manage staff members
|
||||
- Set up channels and services
|
||||
- Monitor appointments and bookings
|
||||
|
||||
Register now to get started:
|
||||
{{registrationUrl}}
|
||||
|
||||
This invitation is valid for 7 days. If you didn't request this email, you can ignore it.
|
||||
|
||||
--
|
||||
Open Reception Team
|
||||
@@ -9,8 +9,6 @@ import { eq, and, not, count, or } from "drizzle-orm";
|
||||
import logger from "$lib/logger";
|
||||
import z from "zod/v4";
|
||||
import { ValidationError, NotFoundError, ConflictError } from "../utils/errors";
|
||||
import { sendTenantAdminInviteEmail } from "../email/email-service";
|
||||
import { ERRORS } from "$lib/errors";
|
||||
|
||||
if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set");
|
||||
|
||||
@@ -20,7 +18,6 @@ const tenantCreationSchema = z.object({
|
||||
.min(4)
|
||||
.max(15)
|
||||
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/),
|
||||
inviteAdmin: z.email().optional(),
|
||||
});
|
||||
|
||||
export type TenantCreationRequest = z.infer<typeof tenantCreationSchema>;
|
||||
@@ -67,7 +64,6 @@ export class TenantAdminService {
|
||||
|
||||
// Check if tenant can be created without any duplications. Do not create tenant if:
|
||||
// - short name already exists
|
||||
// - invited tenant admin email already exists
|
||||
const tenantExists = await centralDb
|
||||
.select()
|
||||
.from(centralSchema.tenant)
|
||||
@@ -75,15 +71,6 @@ export class TenantAdminService {
|
||||
if (tenantExists.length > 0) {
|
||||
throw new ConflictError("Tenant with shortname already exists");
|
||||
}
|
||||
if (request.inviteAdmin) {
|
||||
const adminExists = await centralDb
|
||||
.select()
|
||||
.from(centralSchema.user)
|
||||
.where(eq(centralSchema.user.email, request.inviteAdmin));
|
||||
if (adminExists.length > 0) {
|
||||
throw new ConflictError(ERRORS.USERS.EMAIL_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
const configuration = TenantAdminService.getConfigDefaults();
|
||||
|
||||
@@ -150,40 +137,6 @@ export class TenantAdminService {
|
||||
|
||||
log.debug("Tenant service created successfully", { tenantId: tenant[0].id });
|
||||
|
||||
// Send tenant admin invitation email if email is provided
|
||||
if (request.inviteAdmin) {
|
||||
try {
|
||||
// For now, we'll use the email as name. In a real implementation,
|
||||
// you might want to collect the name separately or parse it from the email
|
||||
const adminName = request.inviteAdmin.split("@")[0];
|
||||
|
||||
// Generate registration URL for the tenant admin
|
||||
// This should point to a registration page that pre-fills tenant info
|
||||
const registrationUrl = `${env.PUBLIC_APP_URL || "http://localhost:5173"}/register?tenant=${tenant[0].id}&email=${encodeURIComponent(request.inviteAdmin)}&role=TENANT_ADMIN`;
|
||||
|
||||
await sendTenantAdminInviteEmail(
|
||||
request.inviteAdmin,
|
||||
adminName,
|
||||
tenant[0],
|
||||
registrationUrl,
|
||||
);
|
||||
|
||||
log.info("Tenant admin invitation email sent successfully", {
|
||||
tenantId: tenant[0].id,
|
||||
adminEmail: request.inviteAdmin,
|
||||
});
|
||||
} catch (emailError) {
|
||||
log.error("Failed to send tenant admin invitation email", {
|
||||
tenantId: tenant[0].id,
|
||||
adminEmail: request.inviteAdmin,
|
||||
error: String(emailError),
|
||||
});
|
||||
|
||||
// Don't fail the tenant creation if email fails
|
||||
// Just log the error and continue
|
||||
}
|
||||
}
|
||||
|
||||
return tenantService;
|
||||
} catch (error) {
|
||||
log.error("Failed to create tenant", {
|
||||
|
||||
@@ -53,7 +53,8 @@ export const actions: Actions = {
|
||||
});
|
||||
}
|
||||
|
||||
const resp = await event.fetch(`/api/tenants`, {
|
||||
// Create tenant first
|
||||
const tenantResponse = await event.fetch(`/api/tenants`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -61,16 +62,13 @@ export const actions: Actions = {
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
shortName: form.data.shortName,
|
||||
inviteAdmin: form.data.email,
|
||||
}),
|
||||
});
|
||||
|
||||
if (resp.status < 400) {
|
||||
return { form };
|
||||
} else {
|
||||
if (tenantResponse.status >= 400) {
|
||||
let error = "Unknown error";
|
||||
try {
|
||||
const body = await resp.json();
|
||||
const body = await tenantResponse.json();
|
||||
error = body.error;
|
||||
} catch (e) {
|
||||
log.error("Failed to parse add tenant error response", { error: e });
|
||||
@@ -80,6 +78,38 @@ export const actions: Actions = {
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
// If admin invitation requested, create user invitation
|
||||
if (form.data.inviteAdmin && form.data.email) {
|
||||
const tenantData = await tenantResponse.json();
|
||||
const tenantId = tenantData.tenant?.id;
|
||||
|
||||
if (tenantId) {
|
||||
const userInviteResponse = await event.fetch(`/api/auth/invite`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
email: form.data.email,
|
||||
name: form.data.email.split("@")[0],
|
||||
role: "TENANT_ADMIN",
|
||||
tenantId: tenantId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (userInviteResponse.status >= 400) {
|
||||
log.warn("Tenant created but user invitation failed", {
|
||||
tenantId,
|
||||
email: form.data.email,
|
||||
});
|
||||
// Don't fail the entire operation, just log the warning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { form };
|
||||
},
|
||||
edit: async (event) => {
|
||||
const form = await superValidate(event, zod(editFormSchema));
|
||||
|
||||
@@ -28,12 +28,6 @@ registerOpenAPIRoute("/tenants", "POST", {
|
||||
description: "Short name for the tenant (4-15 characters)",
|
||||
example: "acme-corp",
|
||||
},
|
||||
inviteAdmin: {
|
||||
type: "string",
|
||||
format: "email",
|
||||
description: "Email address to invite as tenant admin",
|
||||
example: "admin@acme-corp.com",
|
||||
},
|
||||
},
|
||||
required: ["shortName"],
|
||||
},
|
||||
@@ -49,15 +43,23 @@ registerOpenAPIRoute("/tenants", "POST", {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: { type: "string", description: "Success message" },
|
||||
tenantId: { type: "string", description: "Generated tenant ID" },
|
||||
shortName: { type: "string", description: "Tenant short name" },
|
||||
tenant: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Generated tenant ID" },
|
||||
shortName: { type: "string", description: "Tenant short name" },
|
||||
},
|
||||
required: ["id", "shortName"],
|
||||
},
|
||||
},
|
||||
required: ["message", "tenantId", "shortName"],
|
||||
required: ["message", "tenant"],
|
||||
},
|
||||
example: {
|
||||
message: "Tenant created successfully",
|
||||
tenantId: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
shortName: "acme-corp",
|
||||
tenant: {
|
||||
id: "01234567-89ab-cdef-0123-456789abcdef",
|
||||
shortName: "acme-corp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -157,14 +159,12 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
|
||||
log.debug("Creating tenant", {
|
||||
shortName: body.shortName,
|
||||
hasInviteAdmin: !!body.inviteAdmin,
|
||||
});
|
||||
|
||||
checkPermission(locals, null, true);
|
||||
|
||||
const tenantService = await TenantAdminService.createTenant({
|
||||
shortName: body.shortName,
|
||||
inviteAdmin: body.inviteAdmin,
|
||||
});
|
||||
|
||||
log.debug("Tenant created successfully", {
|
||||
@@ -175,8 +175,10 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
return json(
|
||||
{
|
||||
message: "Tenant created successfully",
|
||||
tenantId: tenantService.tenantId,
|
||||
shortName: body.shortName,
|
||||
tenant: {
|
||||
id: tenantService.tenantId,
|
||||
shortName: body.shortName,
|
||||
},
|
||||
},
|
||||
{ status: 201 },
|
||||
);
|
||||
|
||||
@@ -59,7 +59,6 @@ describe("/api/tenants", () => {
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
shortName: "test-tenant",
|
||||
inviteAdmin: "admin@test.com",
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -87,13 +86,14 @@ describe("/api/tenants", () => {
|
||||
|
||||
expect(TenantAdminService.createTenant).toHaveBeenCalledWith({
|
||||
shortName: "test-tenant",
|
||||
inviteAdmin: "admin@test.com",
|
||||
});
|
||||
|
||||
expect(data).toEqual({
|
||||
message: "Tenant created successfully",
|
||||
tenantId: "test-tenant-id",
|
||||
shortName: "test-tenant",
|
||||
tenant: {
|
||||
id: "test-tenant-id",
|
||||
shortName: "test-tenant",
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
@@ -142,8 +142,10 @@ describe("/api/tenants", () => {
|
||||
|
||||
expect(data).toEqual({
|
||||
message: "Tenant created successfully",
|
||||
tenantId: "test-tenant-id-2",
|
||||
shortName: "test-tenant-2",
|
||||
tenant: {
|
||||
id: "test-tenant-id-2",
|
||||
shortName: "test-tenant-2",
|
||||
},
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user