mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 04:26:42 +02:00
fix(ci): build shared packages before tests and fix formatting
- Add build:packages step to all test.yml jobs (fixes @manacore/shared-nestjs-auth not found) - Handle missing coverage artifacts gracefully in test-coverage.yml - Update .prettierignore to exclude apps-archived/ and problematic files - Format all source files to pass CI checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5282f5545b
commit
0ebfde0851
163 changed files with 15247 additions and 14677 deletions
|
|
@ -9,6 +9,7 @@ Mana Core Auth is the central authentication service for the Mana Universe ecosy
|
|||
### 1. ALWAYS USE BETTER AUTH - NO EXCEPTIONS
|
||||
|
||||
**DO NOT** implement custom authentication logic. Better Auth handles:
|
||||
|
||||
- User registration and sign-in
|
||||
- JWT token generation (EdDSA algorithm)
|
||||
- JWT token verification (via JWKS)
|
||||
|
|
@ -19,13 +20,13 @@ Mana Core Auth is the central authentication service for the Mana Universe ecosy
|
|||
|
||||
### 2. JWT Rules
|
||||
|
||||
| DO | DON'T |
|
||||
|----|-------|
|
||||
| Use `jose` library for JWT operations | Use `jsonwebtoken` library |
|
||||
| Use Better Auth's JWKS endpoint | Configure RSA keys in `.env` |
|
||||
| Use EdDSA algorithm (Better Auth default) | Use RS256 or HS256 |
|
||||
| Fetch JWKS from `/api/v1/auth/jwks` | Hardcode public keys |
|
||||
| Keep JWT claims minimal | Add credit_balance, org data to JWT |
|
||||
| DO | DON'T |
|
||||
| ----------------------------------------- | ----------------------------------- |
|
||||
| Use `jose` library for JWT operations | Use `jsonwebtoken` library |
|
||||
| Use Better Auth's JWKS endpoint | Configure RSA keys in `.env` |
|
||||
| Use EdDSA algorithm (Better Auth default) | Use RS256 or HS256 |
|
||||
| Fetch JWKS from `/api/v1/auth/jwks` | Hardcode public keys |
|
||||
| Keep JWT claims minimal | Add credit_balance, org data to JWT |
|
||||
|
||||
### 3. Before Making Auth Changes
|
||||
|
||||
|
|
@ -100,12 +101,12 @@ services/mana-core-auth/
|
|||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/auth/better-auth.config.ts` | Better Auth configuration with JWT + Org plugins |
|
||||
| `src/auth/services/better-auth.service.ts` | Main auth service - ALL auth logic here |
|
||||
| `src/db/schema/auth.schema.ts` | User, session, account, jwks tables |
|
||||
| `docs/AUTHENTICATION_ARCHITECTURE.md` | Comprehensive auth documentation |
|
||||
| File | Purpose |
|
||||
| ------------------------------------------ | ------------------------------------------------ |
|
||||
| `src/auth/better-auth.config.ts` | Better Auth configuration with JWT + Org plugins |
|
||||
| `src/auth/services/better-auth.service.ts` | Main auth service - ALL auth logic here |
|
||||
| `src/db/schema/auth.schema.ts` | User, session, account, jwks tables |
|
||||
| `docs/AUTHENTICATION_ARCHITECTURE.md` | Comprehensive auth documentation |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
|
@ -136,6 +137,7 @@ Other services call `POST /api/v1/auth/validate` with the JWT. The validation us
|
|||
### Adding JWT claims
|
||||
|
||||
**DON'T** add dynamic data to JWT claims. Keep them minimal:
|
||||
|
||||
- `sub` (user ID)
|
||||
- `email`
|
||||
- `role`
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ src/db/schema/
|
|||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------------- | ------------------------------------- |
|
||||
| `pnpm db:push` | Sync schema to database |
|
||||
| `pnpm db:studio`| Open Drizzle Studio to view/edit data |
|
||||
| Command | Description |
|
||||
| ---------------- | ------------------------------------- |
|
||||
| `pnpm db:push` | Sync schema to database |
|
||||
| `pnpm db:studio` | Open Drizzle Studio to view/edit data |
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
## Overview
|
||||
|
||||
The Mana Core authentication service uses PostgreSQL with two main schemas:
|
||||
|
||||
- `auth` - User authentication, sessions, and organization management
|
||||
- `credits` - Credit system for B2C and B2B customers
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ credits.organization_balances ←───────────────
|
|||
### Core Tables
|
||||
|
||||
#### auth.organizations
|
||||
|
||||
Stores organization/company information for B2B customers.
|
||||
|
||||
```sql
|
||||
|
|
@ -58,11 +60,13 @@ CREATE TABLE auth.organizations (
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- Uses TEXT for IDs (Better Auth requirement - nanoid/ULID format)
|
||||
- Slug is unique and URL-friendly for organization pages
|
||||
- Metadata field allows flexible custom attributes
|
||||
|
||||
#### auth.members
|
||||
|
||||
Links users to organizations with roles (owner, admin, member).
|
||||
|
||||
```sql
|
||||
|
|
@ -80,11 +84,13 @@ CREATE INDEX members_organization_user_idx ON auth.members(organization_id, user
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- Composite index on (organization_id, user_id) for fast membership checks
|
||||
- user_id is TEXT to match Better Auth expectations (actual data is UUID cast to TEXT)
|
||||
- ON DELETE CASCADE ensures members are removed when org is deleted
|
||||
|
||||
#### auth.invitations
|
||||
|
||||
Tracks pending, accepted, and rejected organization invitations.
|
||||
|
||||
```sql
|
||||
|
|
@ -105,6 +111,7 @@ CREATE INDEX invitations_status_idx ON auth.invitations(status);
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- Index on email for quick lookup of pending invitations
|
||||
- Index on status for filtering active invitations
|
||||
- ON DELETE SET NULL for inviter (keeps history even if inviter deleted)
|
||||
|
|
@ -113,6 +120,7 @@ CREATE INDEX invitations_status_idx ON auth.invitations(status);
|
|||
## Organization Credit Management
|
||||
|
||||
### credits.organization_balances
|
||||
|
||||
Tracks credit pools for B2B organizations.
|
||||
|
||||
```sql
|
||||
|
|
@ -130,6 +138,7 @@ CREATE TABLE credits.organization_balances (
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- `balance`: Organization's total purchased credits
|
||||
- `allocated_credits`: Sum of credits allocated to employees (not yet spent)
|
||||
- `available_credits`: Credits owner can still allocate (calculated: balance - allocated_credits)
|
||||
|
|
@ -138,12 +147,14 @@ CREATE TABLE credits.organization_balances (
|
|||
- `version`: Enables optimistic locking to prevent race conditions
|
||||
|
||||
**Credit Flow:**
|
||||
|
||||
1. Owner purchases credits → `balance` increases
|
||||
2. Owner allocates to employee → `allocated_credits` increases, `available_credits` decreases
|
||||
3. Employee spends credits → employee's `credits.balances.balance` decreases
|
||||
4. Owner deallocates from employee → `allocated_credits` decreases, `available_credits` increases
|
||||
|
||||
### credits.credit_allocations
|
||||
|
||||
Immutable audit trail of all credit allocations.
|
||||
|
||||
```sql
|
||||
|
|
@ -167,6 +178,7 @@ CREATE INDEX credit_allocations_created_at_idx ON credits.credit_allocations(cre
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- **Immutable**: No updates or deletes allowed (audit trail)
|
||||
- `amount` can be positive (allocation) or negative (deallocation/adjustment)
|
||||
- `balance_before`/`balance_after` track exact state changes
|
||||
|
|
@ -174,6 +186,7 @@ CREATE INDEX credit_allocations_created_at_idx ON credits.credit_allocations(cre
|
|||
- `reason` field for transparency and accountability
|
||||
|
||||
### credits.transactions (Updated)
|
||||
|
||||
Extended to support B2B transactions.
|
||||
|
||||
```sql
|
||||
|
|
@ -185,6 +198,7 @@ CREATE INDEX transactions_organization_id_idx ON credits.transactions(organizati
|
|||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- `organization_id` is **nullable** (NULL for B2C users, set for B2B employees)
|
||||
- ON DELETE SET NULL preserves transaction history even if org deleted
|
||||
- Enables organization-wide usage analytics and reporting
|
||||
|
|
@ -194,6 +208,7 @@ CREATE INDEX transactions_organization_id_idx ON credits.transactions(organizati
|
|||
### The UUID vs TEXT Challenge
|
||||
|
||||
**Problem:**
|
||||
|
||||
- Better Auth uses TEXT IDs (nanoid/ULID format like "abc123xyz")
|
||||
- Our existing system uses UUID for user IDs
|
||||
- PostgreSQL doesn't allow direct foreign keys between UUID and TEXT
|
||||
|
|
@ -211,18 +226,19 @@ FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE;
|
|||
```
|
||||
|
||||
**In Application Code:**
|
||||
|
||||
```typescript
|
||||
// When inserting a member
|
||||
await db.insert(members).values({
|
||||
id: nanoid(),
|
||||
organization_id: "org_abc123",
|
||||
user_id: userId.toString(), // Convert UUID to TEXT
|
||||
role: 'member'
|
||||
id: nanoid(),
|
||||
organization_id: 'org_abc123',
|
||||
user_id: userId.toString(), // Convert UUID to TEXT
|
||||
role: 'member',
|
||||
});
|
||||
|
||||
// When querying
|
||||
const member = await db.query.members.findFirst({
|
||||
where: eq(members.userId, userId.toString())
|
||||
where: eq(members.userId, userId.toString()),
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -243,26 +259,31 @@ auth.is_organization_owner(org_id TEXT) → BOOLEAN
|
|||
### Key Policies
|
||||
|
||||
**Organizations:**
|
||||
|
||||
- Members can view their organizations
|
||||
- Any user can create organizations (Better Auth adds them as owner)
|
||||
- Only owners can update/delete organizations
|
||||
|
||||
**Members:**
|
||||
|
||||
- Members can view other members in their orgs
|
||||
- Owners/admins can add/remove/update members
|
||||
- Members can remove themselves
|
||||
|
||||
**Invitations:**
|
||||
|
||||
- Members can view org invitations
|
||||
- Invitees can view invitations sent to them
|
||||
- Owners/admins can create/manage invitations
|
||||
- Inviters and invitees can delete invitations
|
||||
|
||||
**Organization Balances:**
|
||||
|
||||
- Members can view org balance
|
||||
- Only owners can modify balances
|
||||
|
||||
**Credit Allocations:**
|
||||
|
||||
- Employees can view allocations to them
|
||||
- Owners/admins can view all org allocations
|
||||
- Only owners can create allocations
|
||||
|
|
@ -286,16 +307,19 @@ psql $DATABASE_URL -f src/db/migrations/0001_better_auth_organizations.sql
|
|||
### Migration Files
|
||||
|
||||
**Up Migration:** `0001_better_auth_organizations.sql`
|
||||
|
||||
- Creates organization tables
|
||||
- Creates credit management tables
|
||||
- Adds foreign keys and indexes
|
||||
- Sets up triggers
|
||||
|
||||
**Down Migration:** `0001_better_auth_organizations_down.sql`
|
||||
|
||||
- Reverses all changes
|
||||
- Safe rollback path
|
||||
|
||||
**RLS Policies:** `postgres/init/03-organization-rls.sql`
|
||||
|
||||
- Applied automatically in Docker
|
||||
- Can be run manually: `psql $DATABASE_URL -f postgres/init/03-organization-rls.sql`
|
||||
|
||||
|
|
@ -332,6 +356,7 @@ VALUES ('org_abc123');
|
|||
### Indexes
|
||||
|
||||
All critical query paths are indexed:
|
||||
|
||||
- Organization lookups by slug
|
||||
- Member lookups by user_id and organization_id
|
||||
- Invitation lookups by email and status
|
||||
|
|
@ -343,15 +368,18 @@ Both `credits.balances` and `credits.organization_balances` use a `version` colu
|
|||
|
||||
```typescript
|
||||
// Prevent race conditions when allocating credits
|
||||
await db.update(organizationBalances)
|
||||
.set({
|
||||
allocated_credits: sql`allocated_credits + ${amount}`,
|
||||
version: sql`version + 1`
|
||||
})
|
||||
.where(and(
|
||||
eq(organizationBalances.organizationId, orgId),
|
||||
eq(organizationBalances.version, currentVersion)
|
||||
));
|
||||
await db
|
||||
.update(organizationBalances)
|
||||
.set({
|
||||
allocated_credits: sql`allocated_credits + ${amount}`,
|
||||
version: sql`version + 1`,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(organizationBalances.organizationId, orgId),
|
||||
eq(organizationBalances.version, currentVersion)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Schema Relationships
|
||||
|
|
@ -399,12 +427,14 @@ ADD COLUMN credits_expire_at TIMESTAMPTZ;
|
|||
### Common Issues
|
||||
|
||||
**Foreign Key Errors (UUID vs TEXT):**
|
||||
|
||||
```sql
|
||||
-- Check if casting is needed
|
||||
SELECT user_id::uuid FROM auth.members WHERE user_id ~ '^[0-9a-f-]{36}$';
|
||||
```
|
||||
|
||||
**RLS Policy Blocking Queries:**
|
||||
|
||||
```sql
|
||||
-- Temporarily disable RLS for debugging (development only!)
|
||||
ALTER TABLE auth.organizations DISABLE ROW LEVEL SECURITY;
|
||||
|
|
@ -414,17 +444,18 @@ SELECT * FROM pg_policies WHERE tablename = 'organizations';
|
|||
```
|
||||
|
||||
**Optimistic Lock Failures:**
|
||||
|
||||
```typescript
|
||||
// Retry logic for version conflicts
|
||||
const maxRetries = 3;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await allocateCredits(orgId, employeeId, amount);
|
||||
break;
|
||||
} catch (err) {
|
||||
if (i === maxRetries - 1) throw err;
|
||||
await sleep(100 * Math.pow(2, i)); // Exponential backoff
|
||||
}
|
||||
try {
|
||||
await allocateCredits(orgId, employeeId, amount);
|
||||
break;
|
||||
} catch (err) {
|
||||
if (i === maxRetries - 1) throw err;
|
||||
await sleep(100 * Math.pow(2, i)); // Exponential backoff
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ module.exports = {
|
|||
coverageDirectory: '../coverage',
|
||||
testEnvironment: 'node',
|
||||
// Handle ESM modules (nanoid, better-auth)
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(nanoid|better-auth)/)',
|
||||
],
|
||||
transformIgnorePatterns: ['node_modules/(?!(nanoid|better-auth)/)'],
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)$': '<rootDir>/$1',
|
||||
'^nanoid$': '<rootDir>/../test/__mocks__/nanoid.ts',
|
||||
|
|
|
|||
|
|
@ -303,7 +303,12 @@ export const mockMemberFactory = {
|
|||
* Mock Credit Allocation Factory
|
||||
*/
|
||||
export const mockCreditAllocationFactory = {
|
||||
create: (organizationId: string, employeeId: string, allocatedBy: string, overrides: Partial<any> = {}) => ({
|
||||
create: (
|
||||
organizationId: string,
|
||||
employeeId: string,
|
||||
allocatedBy: string,
|
||||
overrides: Partial<any> = {}
|
||||
) => ({
|
||||
id: nanoid(),
|
||||
organizationId,
|
||||
employeeId,
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ export const assertHelpers = {
|
|||
* Assert that a value is a valid UUID
|
||||
*/
|
||||
assertIsUuid: (value: string) => {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
expect(value).toMatch(uuidRegex);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ Antworte NUR mit validem JSON in diesem Format (keine Markdown-Codeblocks, kein
|
|||
|
||||
private fallbackAnalysis(feedbackText: string): FeedbackAnalysis {
|
||||
// Simple fallback: use first 60 chars as title, default category
|
||||
const title =
|
||||
feedbackText.length > 60 ? feedbackText.substring(0, 57) + '...' : feedbackText;
|
||||
const title = feedbackText.length > 60 ? feedbackText.substring(0, 57) + '...' : feedbackText;
|
||||
|
||||
// Simple keyword-based category detection
|
||||
const lowerText = feedbackText.toLowerCase();
|
||||
|
|
@ -88,7 +87,11 @@ Antworte NUR mit validem JSON in diesem Format (keine Markdown-Codeblocks, kein
|
|||
lowerText.includes('funktioniert nicht')
|
||||
) {
|
||||
category = 'bug';
|
||||
} else if (lowerText.includes('?') || lowerText.includes('frage') || lowerText.includes('wie')) {
|
||||
} else if (
|
||||
lowerText.includes('?') ||
|
||||
lowerText.includes('frage') ||
|
||||
lowerText.includes('wie')
|
||||
) {
|
||||
category = 'question';
|
||||
} else if (
|
||||
lowerText.includes('besser') ||
|
||||
|
|
|
|||
|
|
@ -341,7 +341,10 @@ describe('AuthController', () => {
|
|||
it('should return invalid for expired token', async () => {
|
||||
const body = { token: 'expired-token' };
|
||||
|
||||
betterAuthService.validateToken.mockResolvedValue({ valid: false, error: 'Token expired' } as any);
|
||||
betterAuthService.validateToken.mockResolvedValue({
|
||||
valid: false,
|
||||
error: 'Token expired',
|
||||
} as any);
|
||||
|
||||
const result = await controller.validate(body);
|
||||
|
||||
|
|
@ -483,7 +486,11 @@ describe('AuthController', () => {
|
|||
describe('POST /auth/organizations/:id/invite', () => {
|
||||
it('should invite an employee to organization', async () => {
|
||||
const orgId = 'org-123';
|
||||
const inviteDto = { organizationId: orgId, employeeEmail: 'employee@acme.com', role: 'member' as const };
|
||||
const inviteDto = {
|
||||
organizationId: orgId,
|
||||
employeeEmail: 'employee@acme.com',
|
||||
role: 'member' as const,
|
||||
};
|
||||
|
||||
const expectedResult = {
|
||||
id: 'invitation-123',
|
||||
|
|
@ -508,7 +515,11 @@ describe('AuthController', () => {
|
|||
|
||||
it('should throw ForbiddenException when inviter lacks permission', async () => {
|
||||
const orgId = 'org-123';
|
||||
const inviteDto = { organizationId: orgId, employeeEmail: 'employee@acme.com', role: 'member' as const };
|
||||
const inviteDto = {
|
||||
organizationId: orgId,
|
||||
employeeEmail: 'employee@acme.com',
|
||||
role: 'member' as const,
|
||||
};
|
||||
|
||||
betterAuthService.inviteEmployee.mockRejectedValue(
|
||||
new ForbiddenException('You do not have permission to invite members')
|
||||
|
|
@ -585,9 +596,9 @@ describe('AuthController', () => {
|
|||
new ForbiddenException('You do not have permission to remove members')
|
||||
);
|
||||
|
||||
await expect(controller.removeMember('org-123', 'member-456', mockAuthHeader)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
await expect(
|
||||
controller.removeMember('org-123', 'member-456', mockAuthHeader)
|
||||
).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,8 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|||
import { jwt } from 'better-auth/plugins/jwt';
|
||||
import { organization } from 'better-auth/plugins/organization';
|
||||
import { getDb } from '../db/connection';
|
||||
import {
|
||||
organizations,
|
||||
members,
|
||||
invitations,
|
||||
} from '../db/schema/organizations.schema';
|
||||
import {
|
||||
users,
|
||||
sessions,
|
||||
accounts,
|
||||
verificationTokens,
|
||||
jwks,
|
||||
} from '../db/schema/auth.schema';
|
||||
import { organizations, members, invitations } from '../db/schema/organizations.schema';
|
||||
import { users, sessions, accounts, verificationTokens, jwks } from '../db/schema/auth.schema';
|
||||
import type { JWTPayloadContext } from './types/better-auth.types';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -287,9 +287,9 @@ describe('JWT Token Validation (Minimal Claims)', () => {
|
|||
|
||||
// Tamper with the token - try to change role to admin
|
||||
const parts = token.split('.');
|
||||
const tamperedPayload = Buffer.from(
|
||||
JSON.stringify({ ...payload, role: 'admin' })
|
||||
).toString('base64url');
|
||||
const tamperedPayload = Buffer.from(JSON.stringify({ ...payload, role: 'admin' })).toString(
|
||||
'base64url'
|
||||
);
|
||||
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
|
||||
|
||||
expect(() => {
|
||||
|
|
|
|||
|
|
@ -11,11 +11,7 @@
|
|||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ConflictException,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { ConflictException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { BetterAuthService } from './better-auth.service';
|
||||
import { createMockConfigService } from '../../__tests__/utils/test-helpers';
|
||||
|
||||
|
|
@ -156,9 +152,7 @@ describe('BetterAuthService', () => {
|
|||
};
|
||||
|
||||
// Mock Better Auth error for existing user
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(
|
||||
new Error('User with this email already exists')
|
||||
);
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(new Error('User with this email already exists'));
|
||||
|
||||
await expect(service.registerB2C(registerDto)).rejects.toThrow(ConflictException);
|
||||
await expect(service.registerB2C(registerDto)).rejects.toThrow(
|
||||
|
|
@ -365,9 +359,7 @@ describe('BetterAuthService', () => {
|
|||
});
|
||||
|
||||
// Mock organization creation failure
|
||||
mockAuthApi.createOrganization.mockRejectedValue(
|
||||
new Error('Failed to create organization')
|
||||
);
|
||||
mockAuthApi.createOrganization.mockRejectedValue(new Error('Failed to create organization'));
|
||||
|
||||
await expect(service.registerB2B(registerDto)).rejects.toThrow(
|
||||
'Failed to create organization'
|
||||
|
|
@ -411,9 +403,7 @@ describe('BetterAuthService', () => {
|
|||
organizationName: 'Acme Corporation',
|
||||
};
|
||||
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(
|
||||
new Error('User with this email already exists')
|
||||
);
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(new Error('User with this email already exists'));
|
||||
|
||||
await expect(service.registerB2B(registerDto)).rejects.toThrow(ConflictException);
|
||||
await expect(service.registerB2B(registerDto)).rejects.toThrow('Owner email already exists');
|
||||
|
|
@ -526,13 +516,9 @@ describe('BetterAuthService', () => {
|
|||
inviterToken: 'inviter-token',
|
||||
};
|
||||
|
||||
mockAuthApi.inviteMember.mockRejectedValue(
|
||||
new Error('User is already a member')
|
||||
);
|
||||
mockAuthApi.inviteMember.mockRejectedValue(new Error('User is already a member'));
|
||||
|
||||
await expect(service.inviteEmployee(inviteDto)).rejects.toThrow(
|
||||
'User is already a member'
|
||||
);
|
||||
await expect(service.inviteEmployee(inviteDto)).rejects.toThrow('User is already a member');
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if inviter lacks permission', async () => {
|
||||
|
|
@ -588,9 +574,7 @@ describe('BetterAuthService', () => {
|
|||
userToken: 'user-token',
|
||||
};
|
||||
|
||||
mockAuthApi.acceptInvitation.mockRejectedValue(
|
||||
new Error('Invitation expired')
|
||||
);
|
||||
mockAuthApi.acceptInvitation.mockRejectedValue(new Error('Invitation expired'));
|
||||
|
||||
await expect(service.acceptInvitation(acceptDto)).rejects.toThrow(NotFoundException);
|
||||
await expect(service.acceptInvitation(acceptDto)).rejects.toThrow(
|
||||
|
|
@ -604,9 +588,7 @@ describe('BetterAuthService', () => {
|
|||
userToken: 'user-token',
|
||||
};
|
||||
|
||||
mockAuthApi.acceptInvitation.mockRejectedValue(
|
||||
new Error('Invitation not found')
|
||||
);
|
||||
mockAuthApi.acceptInvitation.mockRejectedValue(new Error('Invitation not found'));
|
||||
|
||||
await expect(service.acceptInvitation(acceptDto)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
|
@ -652,9 +634,7 @@ describe('BetterAuthService', () => {
|
|||
});
|
||||
|
||||
it('should return empty array on error', async () => {
|
||||
mockAuthApi.getFullOrganization.mockRejectedValue(
|
||||
new Error('Database error')
|
||||
);
|
||||
mockAuthApi.getFullOrganization.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const result = await service.getOrganizationMembers('org-123');
|
||||
|
||||
|
|
@ -698,13 +678,9 @@ describe('BetterAuthService', () => {
|
|||
removerToken: 'admin-token',
|
||||
};
|
||||
|
||||
mockAuthApi.removeMember.mockRejectedValue(
|
||||
new Error('Member not found')
|
||||
);
|
||||
mockAuthApi.removeMember.mockRejectedValue(new Error('Member not found'));
|
||||
|
||||
await expect(service.removeMember(removeDto)).rejects.toThrow(
|
||||
'Member not found'
|
||||
);
|
||||
await expect(service.removeMember(removeDto)).rejects.toThrow('Member not found');
|
||||
});
|
||||
|
||||
it('should throw ForbiddenException if remover lacks permission', async () => {
|
||||
|
|
@ -782,9 +758,7 @@ describe('BetterAuthService', () => {
|
|||
new Error('Organization not found or you are not a member')
|
||||
);
|
||||
|
||||
await expect(service.setActiveOrganization(setActiveDto)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
await expect(service.setActiveOrganization(setActiveDto)).rejects.toThrow(NotFoundException);
|
||||
await expect(service.setActiveOrganization(setActiveDto)).rejects.toThrow(
|
||||
'Organization not found or you are not a member'
|
||||
);
|
||||
|
|
@ -970,13 +944,9 @@ describe('BetterAuthService', () => {
|
|||
name: 'Test User',
|
||||
};
|
||||
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(
|
||||
new Error('Unexpected server error')
|
||||
);
|
||||
mockAuthApi.signUpEmail.mockRejectedValue(new Error('Unexpected server error'));
|
||||
|
||||
await expect(service.registerB2C(registerDto)).rejects.toThrow(
|
||||
'Unexpected server error'
|
||||
);
|
||||
await expect(service.registerB2C(registerDto)).rejects.toThrow('Unexpected server error');
|
||||
});
|
||||
|
||||
it('should propagate network errors', async () => {
|
||||
|
|
@ -987,13 +957,9 @@ describe('BetterAuthService', () => {
|
|||
inviterToken: 'token',
|
||||
};
|
||||
|
||||
mockAuthApi.inviteMember.mockRejectedValue(
|
||||
new Error('Network timeout')
|
||||
);
|
||||
mockAuthApi.inviteMember.mockRejectedValue(new Error('Network timeout'));
|
||||
|
||||
await expect(service.inviteEmployee(inviteDto)).rejects.toThrow(
|
||||
'Network timeout'
|
||||
);
|
||||
await expect(service.inviteEmployee(inviteDto)).rejects.toThrow('Network timeout');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,13 +24,7 @@ import { ConfigService } from '@nestjs/config';
|
|||
import { createBetterAuth, type BetterAuthInstance } from '../better-auth.config';
|
||||
import { getDb } from '../../db/connection';
|
||||
import { balances, organizationBalances } from '../../db/schema/credits.schema';
|
||||
import {
|
||||
hasUser,
|
||||
hasToken,
|
||||
hasMember,
|
||||
hasMembers,
|
||||
hasSession,
|
||||
} from '../types/better-auth.types';
|
||||
import { hasUser, hasToken, hasMember, hasMembers, hasSession } from '../types/better-auth.types';
|
||||
import type {
|
||||
RegisterB2CDto,
|
||||
RegisterB2BDto,
|
||||
|
|
|
|||
|
|
@ -360,9 +360,7 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(BadRequestException);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
'Insufficient credits'
|
||||
);
|
||||
|
|
@ -387,9 +385,7 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(NotFoundException);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
'User balance not found'
|
||||
);
|
||||
|
|
@ -436,9 +432,7 @@ describe('CreditsService', () => {
|
|||
values: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -477,9 +471,7 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
ConflictException
|
||||
);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(ConflictException);
|
||||
await expect(service.useCredits(userId, useCreditsDto)).rejects.toThrow(
|
||||
'Balance was modified by another transaction'
|
||||
);
|
||||
|
|
@ -551,9 +543,7 @@ describe('CreditsService', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -615,9 +605,7 @@ describe('CreditsService', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -795,12 +783,7 @@ describe('CreditsService', () => {
|
|||
lastDailyResetAt: lastMonth,
|
||||
});
|
||||
|
||||
mockDb.mockResults(
|
||||
[mockBalance],
|
||||
[],
|
||||
[],
|
||||
[{ ...mockBalance, freeCreditsRemaining: 55 }]
|
||||
);
|
||||
mockDb.mockResults([mockBalance], [], [], [{ ...mockBalance, freeCreditsRemaining: 55 }]);
|
||||
|
||||
await service.getBalance(userId);
|
||||
|
||||
|
|
@ -863,9 +846,7 @@ describe('CreditsService', () => {
|
|||
values: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId, { amount: 0 }),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId, { amount: 0 })]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -908,9 +889,7 @@ describe('CreditsService', () => {
|
|||
values: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -1126,9 +1105,9 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.allocateCredits(allocatorUserId, allocateDto)
|
||||
).rejects.toThrow(ForbiddenException);
|
||||
await expect(service.allocateCredits(allocatorUserId, allocateDto)).rejects.toThrow(
|
||||
ForbiddenException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if org has insufficient available credits', async () => {
|
||||
|
|
@ -1167,9 +1146,9 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.allocateCredits(allocatorUserId, allocateDto)
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(service.allocateCredits(allocatorUserId, allocateDto)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
});
|
||||
|
||||
it('should auto-create employee balance if it does not exist', async () => {
|
||||
|
|
@ -1530,12 +1509,12 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.allocateCredits(allocatorUserId, allocateDto)
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(
|
||||
service.allocateCredits(allocatorUserId, allocateDto)
|
||||
).rejects.toThrow('Organization balance was modified by another transaction');
|
||||
await expect(service.allocateCredits(allocatorUserId, allocateDto)).rejects.toThrow(
|
||||
ConflictException
|
||||
);
|
||||
await expect(service.allocateCredits(allocatorUserId, allocateDto)).rejects.toThrow(
|
||||
'Organization balance was modified by another transaction'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1671,12 +1650,12 @@ describe('CreditsService', () => {
|
|||
// Mock: No org balance found - .limit(1) returns empty
|
||||
mockDb.limit.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
service.getOrganizationBalance(organizationId)
|
||||
).rejects.toThrow(NotFoundException);
|
||||
await expect(
|
||||
service.getOrganizationBalance(organizationId)
|
||||
).rejects.toThrow('Organization balance not found');
|
||||
await expect(service.getOrganizationBalance(organizationId)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
await expect(service.getOrganizationBalance(organizationId)).rejects.toThrow(
|
||||
'Organization balance not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1716,9 +1695,7 @@ describe('CreditsService', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -1769,9 +1746,7 @@ describe('CreditsService', () => {
|
|||
}),
|
||||
};
|
||||
|
||||
txMock.returning.mockResolvedValue([
|
||||
mockTransactionFactory.create(userId),
|
||||
]);
|
||||
txMock.returning.mockResolvedValue([mockTransactionFactory.create(userId)]);
|
||||
|
||||
return callback(txMock);
|
||||
});
|
||||
|
|
@ -1843,12 +1818,12 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.deductCredits(userId, useCreditsDto, organizationId)
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(
|
||||
service.deductCredits(userId, useCreditsDto, organizationId)
|
||||
).rejects.toThrow('Balance was modified by another transaction');
|
||||
await expect(service.deductCredits(userId, useCreditsDto, organizationId)).rejects.toThrow(
|
||||
ConflictException
|
||||
);
|
||||
await expect(service.deductCredits(userId, useCreditsDto, organizationId)).rejects.toThrow(
|
||||
'Balance was modified by another transaction'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle insufficient credits error', async () => {
|
||||
|
|
@ -1876,12 +1851,12 @@ describe('CreditsService', () => {
|
|||
return callback(txMock);
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.deductCredits(userId, useCreditsDto, organizationId)
|
||||
).rejects.toThrow(BadRequestException);
|
||||
await expect(
|
||||
service.deductCredits(userId, useCreditsDto, organizationId)
|
||||
).rejects.toThrow('Insufficient credits');
|
||||
await expect(service.deductCredits(userId, useCreditsDto, organizationId)).rejects.toThrow(
|
||||
BadRequestException
|
||||
);
|
||||
await expect(service.deductCredits(userId, useCreditsDto, organizationId)).rejects.toThrow(
|
||||
'Insufficient credits'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -341,18 +341,11 @@ export class CreditsService {
|
|||
const [member] = await tx
|
||||
.select()
|
||||
.from(members)
|
||||
.where(
|
||||
and(
|
||||
eq(members.organizationId, organizationId),
|
||||
eq(members.userId, allocatorUserId)
|
||||
)
|
||||
)
|
||||
.where(and(eq(members.organizationId, organizationId), eq(members.userId, allocatorUserId)))
|
||||
.limit(1);
|
||||
|
||||
if (!member || member.role !== 'owner') {
|
||||
throw new ForbiddenException(
|
||||
'Only organization owners can allocate credits'
|
||||
);
|
||||
throw new ForbiddenException('Only organization owners can allocate credits');
|
||||
}
|
||||
|
||||
// 2. Get organization balance with row lock
|
||||
|
|
@ -441,12 +434,7 @@ export class CreditsService {
|
|||
version: employeeBalance.version + 1,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(balances.userId, employeeId),
|
||||
eq(balances.version, employeeBalance.version)
|
||||
)
|
||||
)
|
||||
.where(and(eq(balances.userId, employeeId), eq(balances.version, employeeBalance.version)))
|
||||
.returning();
|
||||
|
||||
if (updateEmployeeResult.length === 0) {
|
||||
|
|
@ -506,11 +494,7 @@ export class CreditsService {
|
|||
const db = this.getDb();
|
||||
|
||||
// Get employee's personal balance
|
||||
const [balance] = await db
|
||||
.select()
|
||||
.from(balances)
|
||||
.where(eq(balances.userId, userId))
|
||||
.limit(1);
|
||||
const [balance] = await db.select().from(balances).where(eq(balances.userId, userId)).limit(1);
|
||||
|
||||
if (!balance) {
|
||||
return null;
|
||||
|
|
@ -571,11 +555,7 @@ export class CreditsService {
|
|||
* Deduct credits with organization tracking
|
||||
* Enhanced version of useCredits that tracks organization_id for B2B users
|
||||
*/
|
||||
async deductCredits(
|
||||
userId: string,
|
||||
useCreditsDto: UseCreditsDto,
|
||||
organizationId?: string
|
||||
) {
|
||||
async deductCredits(userId: string, useCreditsDto: UseCreditsDto, organizationId?: string) {
|
||||
const db = this.getDb();
|
||||
|
||||
// Check for idempotency
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,13 @@
|
|||
import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum, index } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgSchema,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
index,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
export const authSchema = pgSchema('auth');
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,7 @@ export class FeedbackController {
|
|||
|
||||
@Get('my')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getMyFeedback(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Query('appId') appId?: string
|
||||
) {
|
||||
async getMyFeedback(@CurrentUser() user: CurrentUserData, @Query('appId') appId?: string) {
|
||||
return this.feedbackService.getMyFeedback(user.userId, appId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export class FeedbackService {
|
|||
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
|
||||
)
|
||||
)
|
||||
: [];
|
||||
: [];
|
||||
|
||||
votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ export class FeedbackService {
|
|||
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
|
||||
)
|
||||
)
|
||||
: [];
|
||||
: [];
|
||||
|
||||
const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ import { randomBytes } from 'crypto';
|
|||
|
||||
// Helper to generate random IDs (avoiding nanoid ESM issues in Jest)
|
||||
const generateId = (length: number = 16): string => {
|
||||
return randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
|
||||
return randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length);
|
||||
};
|
||||
|
||||
describe('B2B Organization Journey (E2E)', () => {
|
||||
|
|
|
|||
|
|
@ -486,19 +486,13 @@ describe('B2C User Journey (E2E)', () => {
|
|||
});
|
||||
|
||||
it('should reject SQL injection attempts in email field', async () => {
|
||||
const sqlInjectionPayloads = [
|
||||
"admin'--",
|
||||
"' OR '1'='1",
|
||||
"'; DROP TABLE users; --",
|
||||
];
|
||||
const sqlInjectionPayloads = ["admin'--", "' OR '1'='1", "'; DROP TABLE users; --"];
|
||||
|
||||
for (const payload of sqlInjectionPayloads) {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: payload,
|
||||
password: 'SomePassword123!',
|
||||
});
|
||||
const response = await request(app.getHttpServer()).post('/auth/login').send({
|
||||
email: payload,
|
||||
password: 'SomePassword123!',
|
||||
});
|
||||
|
||||
// Should fail safely without SQL injection
|
||||
expect([400, 401]).toContain(response.status);
|
||||
|
|
|
|||
|
|
@ -217,9 +217,7 @@ describe('Authentication Flow Integration Tests', () => {
|
|||
await authService.logout(sessionId);
|
||||
|
||||
// Attempt to refresh with revoked token
|
||||
await expect(authService.refreshToken(refreshToken)).rejects.toThrow(
|
||||
'Invalid refresh token'
|
||||
);
|
||||
await expect(authService.refreshToken(refreshToken)).rejects.toThrow('Invalid refresh token');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -165,9 +165,7 @@ describe('Credit Flow Integration Tests', () => {
|
|||
// Balance should be unchanged
|
||||
const balanceAfterSecond = await creditsService.getBalance(userId);
|
||||
|
||||
expect(balanceAfterSecond.freeCreditsRemaining).toBe(
|
||||
balanceAfterFirst.freeCreditsRemaining
|
||||
);
|
||||
expect(balanceAfterSecond.freeCreditsRemaining).toBe(balanceAfterFirst.freeCreditsRemaining);
|
||||
expect(balanceAfterSecond.totalSpent).toBe(balanceAfterFirst.totalSpent);
|
||||
});
|
||||
|
||||
|
|
@ -447,9 +445,7 @@ describe('Credit Flow Integration Tests', () => {
|
|||
// Balance should be unchanged
|
||||
const balanceAfterError = await creditsService.getBalance(userId);
|
||||
|
||||
expect(balanceAfterError.freeCreditsRemaining).toBe(
|
||||
initialBalance.freeCreditsRemaining
|
||||
);
|
||||
expect(balanceAfterError.freeCreditsRemaining).toBe(initialBalance.freeCreditsRemaining);
|
||||
expect(balanceAfterError.balance).toBe(initialBalance.balance);
|
||||
expect(balanceAfterError.totalSpent).toBe(initialBalance.totalSpent);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@
|
|||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": ["ts-jest", {
|
||||
"tsconfig": {
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"^.+\\.(t|j)s$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"tsconfig": {
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
}]
|
||||
]
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(nanoid|better-auth)/)"
|
||||
],
|
||||
"transformIgnorePatterns": ["node_modules/(?!(nanoid|better-auth)/)"],
|
||||
"moduleNameMapper": {
|
||||
"^nanoid$": "<rootDir>/__mocks__/nanoid.ts",
|
||||
"^better-auth$": "<rootDir>/__mocks__/better-auth.ts",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ jest.setTimeout(30000);
|
|||
* Generate random ID using crypto
|
||||
*/
|
||||
const generateRandomId = (length: number = 10): string => {
|
||||
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue