Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 653dce747b | |||
| b46e00fefd | |||
| ceb061addf | |||
| 16c8edf7e9 |
+66
-56
@@ -66,9 +66,6 @@ jobs:
|
|||||||
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Install rsync
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y rsync
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
@@ -112,61 +109,74 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
APP_DIR=/home/tyler/route-commerce
|
APP_DIR=/home/tyler/route-commerce
|
||||||
|
|
||||||
# Add server SSH host key if provided
|
# Setup SSH key (secret contains multiline private key)
|
||||||
if [ -n "$SERVER_SSH_KEY" ]; then
|
mkdir -p ~/.ssh
|
||||||
mkdir -p ~/.ssh
|
printf '%s\n' "$SERVER_SSH_KEY" > ~/.ssh/id_ed25519
|
||||||
echo "$SERVER_SSH_KEY" >> ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
ssh-keyscan -H route.crispygoat.com >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
ssh-keyscan route.crispygoat.com >> ~/.ssh/known_hosts 2>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write production env file
|
# Test SSH connection
|
||||||
|
echo "Testing SSH connection..."
|
||||||
|
ssh -o ConnectTimeout=15 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "echo 'SSH OK' && hostname" || { echo "SSH FAILED - check SERVER_SSH_KEY secret"; exit 1; }
|
||||||
|
|
||||||
|
# Create app dir on server
|
||||||
|
ssh tyler@route.crispygoat.com "mkdir -p $APP_DIR/.next $APP_DIR/public"
|
||||||
|
|
||||||
|
# Write production env file using printf (avoids heredoc quoting issues)
|
||||||
|
ENV_FILE=$(mktemp)
|
||||||
{
|
{
|
||||||
printf "DATABASE_URL=%s\n" "$DATABASE_URL"
|
printf 'DATABASE_URL=%s\n' "$DATABASE_URL"
|
||||||
printf "NEXT_PUBLIC_SITE_URL=%s\n" "$NEXT_PUBLIC_SITE_URL"
|
printf 'NEXT_PUBLIC_SITE_URL=%s\n' "$NEXT_PUBLIC_SITE_URL"
|
||||||
printf "NEON_AUTH_BASE_URL=%s\n" "$NEON_AUTH_BASE_URL"
|
printf 'NEON_AUTH_BASE_URL=%s\n' "$NEON_AUTH_BASE_URL"
|
||||||
printf "NEON_AUTH_COOKIE_SECRET=%s\n" "$NEON_AUTH_COOKIE_SECRET"
|
printf 'NEON_AUTH_COOKIE_SECRET=%s\n' "$NEON_AUTH_COOKIE_SECRET"
|
||||||
printf "AUTH_SECRET=%s\n" "$AUTH_SECRET"
|
printf 'AUTH_SECRET=%s\n' "$AUTH_SECRET"
|
||||||
printf "AUTH_URL=%s\n" "$AUTH_URL"
|
printf 'AUTH_URL=%s\n' "$AUTH_URL"
|
||||||
printf "NEXT_PUBLIC_AUTH_URL=%s\n" "$NEXT_PUBLIC_AUTH_URL"
|
printf 'NEXT_PUBLIC_AUTH_URL=%s\n' "$NEXT_PUBLIC_AUTH_URL"
|
||||||
printf "GOOGLE_CLIENT_ID=%s\n" "$GOOGLE_CLIENT_ID"
|
printf 'GOOGLE_CLIENT_ID=%s\n' "$GOOGLE_CLIENT_ID"
|
||||||
printf "GOOGLE_CLIENT_SECRET=%s\n" "$GOOGLE_CLIENT_SECRET"
|
printf 'GOOGLE_CLIENT_SECRET=%s\n' "$GOOGLE_CLIENT_SECRET"
|
||||||
printf "ALLOW_DEV_LOGIN=%s\n" "$ALLOW_DEV_LOGIN"
|
printf 'ALLOW_DEV_LOGIN=%s\n' "$ALLOW_DEV_LOGIN"
|
||||||
printf "ADMIN_ALLOWED_EMAILS=%s\n" "$ADMIN_ALLOWED_EMAILS"
|
printf 'ADMIN_ALLOWED_EMAILS=%s\n' "$ADMIN_ALLOWED_EMAILS"
|
||||||
printf "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=%s\n" "$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"
|
printf 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=%s\n' "$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"
|
||||||
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
printf 'STRIPE_SECRET_KEY=%s\n' "$STRIPE_SECRET_KEY"
|
||||||
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
printf 'STRIPE_WEBHOOK_SECRET=%s\n' "$STRIPE_WEBHOOK_SECRET"
|
||||||
printf "STRIPE_PRICE_STARTER=%s\n" "$STRIPE_PRICE_STARTER"
|
printf 'STRIPE_PRICE_STARTER=%s\n' "$STRIPE_PRICE_STARTER"
|
||||||
printf "STRIPE_PRICE_FARM=%s\n" "$STRIPE_PRICE_FARM"
|
printf 'STRIPE_PRICE_FARM=%s\n' "$STRIPE_PRICE_FARM"
|
||||||
printf "STRIPE_PRICE_ENTERPRISE=%s\n" "$STRIPE_PRICE_ENTERPRISE"
|
printf 'STRIPE_PRICE_ENTERPRISE=%s\n' "$STRIPE_PRICE_ENTERPRISE"
|
||||||
printf "STRIPE_PRICE_HARVEST_REACH=%s\n" "$STRIPE_PRICE_HARVEST_REACH"
|
printf 'STRIPE_PRICE_HARVEST_REACH=%s\n' "$STRIPE_PRICE_HARVEST_REACH"
|
||||||
printf "STRIPE_PRICE_WHOLESALE_PORTAL=%s\n" "$STRIPE_PRICE_WHOLESALE_PORTAL"
|
printf 'STRIPE_PRICE_WHOLESALE_PORTAL=%s\n' "$STRIPE_PRICE_WHOLESALE_PORTAL"
|
||||||
printf "STRIPE_PRICE_WATER_LOG=%s\n" "$STRIPE_PRICE_WATER_LOG"
|
printf 'STRIPE_PRICE_WATER_LOG=%s\n' "$STRIPE_PRICE_WATER_LOG"
|
||||||
printf "STRIPE_PRICE_AI_TOOLS=%s\n" "$STRIPE_PRICE_AI_TOOLS"
|
printf 'STRIPE_PRICE_AI_TOOLS=%s\n' "$STRIPE_PRICE_AI_TOOLS"
|
||||||
printf "STRIPE_PRICE_SQUARE_SYNC=%s\n" "$STRIPE_PRICE_SQUARE_SYNC"
|
printf 'STRIPE_PRICE_SQUARE_SYNC=%s\n' "$STRIPE_PRICE_SQUARE_SYNC"
|
||||||
printf "STRIPE_PRICE_SMS_CAMPAIGNS=%s\n" "$STRIPE_PRICE_SMS_CAMPAIGNS"
|
printf 'STRIPE_PRICE_SMS_CAMPAIGNS=%s\n' "$STRIPE_PRICE_SMS_CAMPAIGNS"
|
||||||
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
printf 'RESEND_API_KEY=%s\n' "$RESEND_API_KEY"
|
||||||
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
printf 'FROM_EMAIL=%s\n' "$FROM_EMAIL"
|
||||||
printf "MINIO_ENDPOINT=%s\n" "$MINIO_ENDPOINT"
|
printf 'MINIO_ENDPOINT=%s\n' "$MINIO_ENDPOINT"
|
||||||
printf "MINIO_REGION=%s\n" "$MINIO_REGION"
|
printf 'MINIO_REGION=%s\n' "$MINIO_REGION"
|
||||||
printf "MINIO_ACCESS_KEY=%s\n" "$MINIO_ACCESS_KEY"
|
printf 'MINIO_ACCESS_KEY=%s\n' "$MINIO_ACCESS_KEY"
|
||||||
printf "MINIO_SECRET_KEY=%s\n" "$MINIO_SECRET_KEY"
|
printf 'MINIO_SECRET_KEY=%s\n' "$MINIO_SECRET_KEY"
|
||||||
printf "MINIO_PUBLIC_URL=%s\n" "$MINIO_PUBLIC_URL"
|
printf 'MINIO_PUBLIC_URL=%s\n' "$MINIO_PUBLIC_URL"
|
||||||
printf "MINIO_BUCKET_PRODUCTS=%s\n" "$MINIO_BUCKET_PRODUCTS"
|
printf 'MINIO_BUCKET_PRODUCTS=%s\n' "$MINIO_BUCKET_PRODUCTS"
|
||||||
printf "MINIO_BUCKET_BRAND_LOGOS=%s\n" "$MINIO_BUCKET_BRAND_LOGOS"
|
printf 'MINIO_BUCKET_BRAND_LOGOS=%s\n' "$MINIO_BUCKET_BRAND_LOGOS"
|
||||||
printf "MINIO_BUCKET_WATER_LOGS=%s\n" "$MINIO_BUCKET_WATER_LOGS"
|
printf 'MINIO_BUCKET_WATER_LOGS=%s\n' "$MINIO_BUCKET_WATER_LOGS"
|
||||||
printf "MINIMAX_API_KEY=%s\n" "$MINIMAX_API_KEY"
|
printf 'MINIMAX_API_KEY=%s\n' "$MINIMAX_API_KEY"
|
||||||
printf "MINIMAX_BASE_URL=%s\n" "$MINIMAX_BASE_URL"
|
printf 'MINIMAX_BASE_URL=%s\n' "$MINIMAX_BASE_URL"
|
||||||
printf "CRON_SECRET=%s\n" "$CRON_SECRET"
|
printf 'CRON_SECRET=%s\n' "$CRON_SECRET"
|
||||||
} > $APP_DIR/.env.production
|
} > "$ENV_FILE"
|
||||||
|
|
||||||
# Sync build output and required files to server
|
# Upload env file and sync build output
|
||||||
rsync -a --delete .next/ tyler@route.crispygoat.com:$APP_DIR/.next/
|
echo "Uploading env file..."
|
||||||
rsync -a --delete public/ tyler@route.crispygoat.com:$APP_DIR/public/
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no "$ENV_FILE" tyler@route.crispygoat.com:$APP_DIR/.env.production
|
||||||
rsync $APP_DIR/package.json tyler@route.crispygoat.com:$APP_DIR/
|
|
||||||
rsync next.config.ts tyler@route.crispygoat.com:$APP_DIR/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Install production deps and restart on server
|
echo "Copying .next/..."
|
||||||
ssh tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev && pm2 restart route-commerce || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save"
|
scp -o ConnectTimeout=30 -o StrictHostKeyChecking=no -r .next tyler@route.crispygoat.com:$APP_DIR/
|
||||||
|
echo "Copying public/..."
|
||||||
|
scp -o ConnectTimeout=30 -o StrictHostKeyChecking=no -r public tyler@route.crispygoat.com:$APP_DIR/
|
||||||
|
echo "Copying package.json..."
|
||||||
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no package.json tyler@route.crispygoat.com:$APP_DIR/
|
||||||
|
scp -o ConnectTimeout=15 -o StrictHostKeyChecking=no next.config.ts tyler@route.crispygoat.com:$APP_DIR/ 2>/dev/null || true
|
||||||
|
|
||||||
echo "Deployed successfully"
|
# Install deps and restart on server
|
||||||
|
echo "Installing deps and restarting PM2..."
|
||||||
|
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && pm2 restart route-commerce || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save"
|
||||||
|
|
||||||
|
echo "Deployed successfully"
|
||||||
@@ -47,45 +47,52 @@ export async function getAdminUser(): Promise<AdminUser | null> {
|
|||||||
|
|
||||||
if (!sessionEmail) return null;
|
if (!sessionEmail) return null;
|
||||||
|
|
||||||
return await withPlatformAdmin(async (db) => {
|
try {
|
||||||
const userRows = await db
|
return await withPlatformAdmin(async (db) => {
|
||||||
.select()
|
const userRows = await db
|
||||||
.from(adminUsers)
|
.select()
|
||||||
.where(eq(adminUsers.email, sessionEmail.toLowerCase()))
|
.from(adminUsers)
|
||||||
.limit(1);
|
.where(eq(adminUsers.email, sessionEmail.toLowerCase()))
|
||||||
const user = userRows[0];
|
.limit(1);
|
||||||
if (!user) return null;
|
const user = userRows[0];
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
const membershipRows = await db
|
const membershipRows = await db
|
||||||
.select({
|
.select({
|
||||||
brandId: adminUserBrands.brandId,
|
brandId: adminUserBrands.brandId,
|
||||||
brandName: brands.name,
|
brandName: brands.name,
|
||||||
brandSlug: brands.slug,
|
brandSlug: brands.slug,
|
||||||
role: adminUserBrands.adminUserId,
|
// Role comes from admin_users table, not admin_user_brands
|
||||||
})
|
role: adminUsers.role,
|
||||||
.from(adminUserBrands)
|
})
|
||||||
.innerJoin(brands, eq(brands.id, adminUserBrands.brandId))
|
.from(adminUserBrands)
|
||||||
.where(eq(adminUserBrands.adminUserId, user.id))
|
.innerJoin(brands, eq(brands.id, adminUserBrands.brandId))
|
||||||
.limit(1);
|
.innerJoin(adminUsers, eq(adminUsers.id, adminUserBrands.adminUserId))
|
||||||
|
.where(eq(adminUserBrands.adminUserId, user.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (membershipRows.length === 0) {
|
if (membershipRows.length === 0) {
|
||||||
// Signed in but not provisioned for any brand.
|
// Signed in but not provisioned for any brand.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = membershipRows[0];
|
const m = membershipRows[0];
|
||||||
const role = user.role as AdminRole;
|
const role = user.role as AdminRole;
|
||||||
return buildAdminUser({
|
return buildAdminUser({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
displayName: user.name,
|
displayName: user.name,
|
||||||
brandId: m.brandId,
|
brandId: m.brandId,
|
||||||
brandName: m.brandName,
|
brandName: m.brandName,
|
||||||
brandSlug: m.brandSlug,
|
brandSlug: m.brandSlug,
|
||||||
role,
|
role,
|
||||||
active: true,
|
active: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error("[admin-permissions] Database query failed:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,58 +6,68 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
// Use vi.hoisted to ensure mocks are available when vi.mock runs
|
||||||
|
const { mockDb, mockWithPlatformAdmin, mockGetSession, mockCookies } = vi.hoisted(() => {
|
||||||
|
const mockDb = {
|
||||||
|
select: vi.fn(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
mockDb,
|
||||||
|
mockWithPlatformAdmin: vi.fn(async (fn: (db: typeof mockDb) => Promise<unknown>) => {
|
||||||
|
return fn(mockDb);
|
||||||
|
}),
|
||||||
|
mockGetSession: vi.fn(),
|
||||||
|
mockCookies: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Stub the server-only guard so the module can be imported under vitest.
|
// Stub the server-only guard so the module can be imported under vitest.
|
||||||
vi.mock("server-only", () => ({}));
|
vi.mock("server-only", () => ({}));
|
||||||
|
|
||||||
// Mock the Drizzle client wrapper so we don't need a real DB.
|
// Mock the Drizzle client wrapper
|
||||||
type MockFn = (arg: unknown) => Promise<unknown>;
|
|
||||||
const mockSelect = vi.fn();
|
|
||||||
const mockWithPlatformAdmin = vi.fn(async (fn: MockFn) => fn({ select: mockSelect }));
|
|
||||||
vi.mock("@/db/client", () => ({
|
vi.mock("@/db/client", () => ({
|
||||||
withPlatformAdmin: (fn: MockFn) => mockWithPlatformAdmin(fn),
|
withPlatformAdmin: mockWithPlatformAdmin,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the getSession() function. The default mock returns null (no session).
|
// Mock the getSession() function
|
||||||
const getSessionMock = vi.fn();
|
|
||||||
vi.mock("@/lib/auth", () => ({
|
vi.mock("@/lib/auth", () => ({
|
||||||
getSession: getSessionMock,
|
getSession: mockGetSession,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock cookies() so we don't read a real cookie store.
|
// Mock cookies() so we don't read a real cookie store
|
||||||
const cookieStoreGet = vi.fn();
|
|
||||||
vi.mock("next/headers", () => ({
|
vi.mock("next/headers", () => ({
|
||||||
cookies: () =>
|
cookies: mockCookies,
|
||||||
Promise.resolve({
|
|
||||||
get: (name: string) => cookieStoreGet(name),
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getAdminUser, buildDevAdmin, permissionsForRole } from "@/lib/admin-permissions";
|
import { getAdminUser, buildDevAdmin, permissionsForRole } from "@/lib/admin-permissions";
|
||||||
|
|
||||||
|
const cookieStore = { get: vi.fn() };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockSelect.mockReset();
|
mockDb.select.mockReset();
|
||||||
getSessionMock.mockReset();
|
mockWithPlatformAdmin.mockReset();
|
||||||
cookieStoreGet.mockReset();
|
mockGetSession.mockReset();
|
||||||
|
mockCookies.mockImplementation(() => Promise.resolve(cookieStore));
|
||||||
|
cookieStore.get.mockReturnValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAdminUser()", () => {
|
describe("getAdminUser()", () => {
|
||||||
it("returns null when there is no Neon Auth session", async () => {
|
it("returns null when there is no Neon Auth session", async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: null });
|
mockGetSession.mockResolvedValue({ data: null });
|
||||||
cookieStoreGet.mockReturnValue(undefined);
|
|
||||||
const u = await getAdminUser();
|
const u = await getAdminUser();
|
||||||
expect(u).toBeNull();
|
expect(u).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when the session has no email", async () => {
|
it("returns null when the session has no email", async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { user: { name: "no-email" } } });
|
mockGetSession.mockResolvedValue({ data: { user: { name: "no-email" } } });
|
||||||
const u = await getAdminUser();
|
const u = await getAdminUser();
|
||||||
expect(u).toBeNull();
|
expect(u).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when the email is not in the users table", async () => {
|
it("returns null when the email is not in the users table", async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { user: { email: "unknown@example.com" } } });
|
mockGetSession.mockResolvedValue({ data: { user: { email: "unknown@example.com" } } });
|
||||||
// First select: users. Returns empty.
|
// First select: users. Returns empty.
|
||||||
mockSelect.mockReturnValueOnce({
|
mockDb.select.mockReturnValueOnce({
|
||||||
from: () => ({
|
from: () => ({
|
||||||
where: () => ({
|
where: () => ({
|
||||||
limit: async () => [],
|
limit: async () => [],
|
||||||
@@ -69,9 +79,9 @@ describe("getAdminUser()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when the user exists but has no admin_user_brands row", async () => {
|
it("returns null when the user exists but has no admin_user_brands row", async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { user: { email: "no-brand@example.com" } } });
|
mockGetSession.mockResolvedValue({ data: { user: { email: "no-brand@example.com" } } });
|
||||||
// First select: users — returns the user
|
// First select: users — returns the user
|
||||||
mockSelect
|
mockDb.select
|
||||||
.mockReturnValueOnce({
|
.mockReturnValueOnce({
|
||||||
from: () => ({
|
from: () => ({
|
||||||
where: () => ({
|
where: () => ({
|
||||||
@@ -80,6 +90,7 @@ describe("getAdminUser()", () => {
|
|||||||
id: "user-1",
|
id: "user-1",
|
||||||
email: "no-brand@example.com",
|
email: "no-brand@example.com",
|
||||||
name: "No Brand",
|
name: "No Brand",
|
||||||
|
role: "brand_admin",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -100,8 +111,8 @@ describe("getAdminUser()", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns a fully-populated AdminUser for a provisioned brand_admin", async () => {
|
it("returns a fully-populated AdminUser for a provisioned brand_admin", async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { user: { email: "admin@tuxedo.example" } } });
|
mockGetSession.mockResolvedValue({ data: { user: { email: "admin@tuxedo.example" } } });
|
||||||
mockSelect
|
mockDb.select
|
||||||
.mockReturnValueOnce({
|
.mockReturnValueOnce({
|
||||||
from: () => ({
|
from: () => ({
|
||||||
where: () => ({
|
where: () => ({
|
||||||
@@ -110,6 +121,7 @@ describe("getAdminUser()", () => {
|
|||||||
id: "user-tux",
|
id: "user-tux",
|
||||||
email: "admin@tuxedo.example",
|
email: "admin@tuxedo.example",
|
||||||
name: "Tux Admin",
|
name: "Tux Admin",
|
||||||
|
role: "brand_admin",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -185,4 +197,4 @@ describe("permissionsForRole()", () => {
|
|||||||
expect(p.can_manage_products).toBe(false);
|
expect(p.can_manage_products).toBe(false);
|
||||||
expect(p.can_manage_billing).toBe(false);
|
expect(p.can_manage_billing).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user