mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Merge branch 'main' into dependabot/npm_and_yarn/storybook-10.1.11
This commit is contained in:
22
README.md
22
README.md
@@ -564,15 +564,27 @@ docker run -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# From frontends/nextjs/
|
||||
npm run test:unit # Vitest unit tests
|
||||
# Unit Tests (from frontends/nextjs/)
|
||||
npm run test:unit # Vitest unit tests (watch mode)
|
||||
npm run test:unit -- --run # Run once (no watch)
|
||||
npm run test:e2e # Playwright E2E tests
|
||||
|
||||
# E2E Tests (from root)
|
||||
npm install # Install dependencies
|
||||
npx playwright install chromium # Install browser (first time only)
|
||||
npm run db:generate # Generate Prisma client (REQUIRED before E2E tests)
|
||||
npm run test:e2e # Run Playwright E2E tests
|
||||
npm run test:e2e:ui # Run with Playwright UI
|
||||
npm run test:e2e:headed # Run with visible browser
|
||||
npm run test:e2e:debug # Debug mode
|
||||
|
||||
# Other
|
||||
npm run lint # ESLint
|
||||
npm run typecheck # TypeScript validation
|
||||
npm run build # Production build
|
||||
```
|
||||
|
||||
> **Note**: E2E tests require Prisma client to be generated first. The playwright config will automatically run `npm run db:generate` before starting the dev server, but you can run it manually if needed.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
@@ -581,8 +593,8 @@ MetaBuilder has a comprehensive testing strategy with unit tests, integration te
|
||||
|
||||
### Test Statistics
|
||||
|
||||
- **Total Tests:** 418 tests across 73 test files
|
||||
- **Pass Rate:** 99.0% (414 passing, 4 failing pre-existing issues)
|
||||
- **Total Tests:** 464 tests across 77 test files
|
||||
- **Pass Rate:** 100% (464 passing, 0 failing)
|
||||
- **Coverage:** Unit, Integration, and E2E tests
|
||||
- **Framework:** Vitest (unit/integration), Playwright (E2E)
|
||||
|
||||
|
||||
51
ROADMAP.md
51
ROADMAP.md
@@ -47,9 +47,9 @@ Browser URL → Database Query → JSON Component → Generic Renderer → React
|
||||
**🎯 Phase:** MVP Achieved ✅ → Phase 2 Backend Integration (In Progress)
|
||||
**Version:** 0.1.0-alpha
|
||||
**Build Status:** Functional
|
||||
**Test Coverage:** 414/418 tests passing (99.0%) - Up from 259/263 (98.5%)
|
||||
**Test Coverage:** 464/464 tests passing (100%) - Up from 414/418 (99.0%)
|
||||
**Last Major Release:** January 2026
|
||||
**Latest Update:** January 8, 2026 - Added retry, pagination, filtering, and validation utilities
|
||||
**Latest Update:** January 8, 2026 - Added pagination components and authentication middleware
|
||||
|
||||
### Quick Stats
|
||||
|
||||
@@ -58,7 +58,7 @@ Browser URL → Database Query → JSON Component → Generic Renderer → React
|
||||
- **Technology:** Next.js 16.1, React 19, TypeScript 5.9, Prisma 7.2
|
||||
- **Architecture:** Multi-tenant, 6-level permissions, data-driven routing
|
||||
- **Services:** Frontend, DBAL (TypeScript + C++), Media Daemon, PostgreSQL, Redis
|
||||
- **Test Suite:** 69 test files, 263 tests (98.5% pass rate)
|
||||
- **Test Suite:** 77 test files, 464 tests (100% pass rate)
|
||||
|
||||
### What's Working Today
|
||||
|
||||
@@ -658,8 +658,8 @@ All criteria met ✅
|
||||
- [x] Common schema patterns (email, uuid, phone, password, username)
|
||||
- [x] Write comprehensive tests for validation (39 tests total)
|
||||
|
||||
##### 2.4 Pagination Implementation 🔨 IN PROGRESS
|
||||
**Status:** Utilities complete, UI components pending (January 8, 2026)
|
||||
##### 2.4 Pagination Implementation ✅ COMPLETE
|
||||
**Status:** ✅ All pagination components and utilities implemented (January 8, 2026)
|
||||
|
||||
- [x] **Pagination Utilities** ✅ COMPLETE
|
||||
- [x] Create pagination helper functions
|
||||
@@ -671,12 +671,14 @@ All criteria met ✅
|
||||
- [x] Page number generation for UI
|
||||
- [x] Write tests for pagination (35 test cases - exceeded target)
|
||||
|
||||
- [ ] **Frontend Pagination Components**
|
||||
- [ ] Create pagination UI component (Material-UI)
|
||||
- [ ] Add page navigation controls
|
||||
- [ ] Add items-per-page selector
|
||||
- [ ] Update list views to use pagination
|
||||
- [ ] Write E2E tests for pagination
|
||||
- [x] **Frontend Pagination Components** ✅ COMPLETE
|
||||
- [x] Create pagination UI component (PaginationControls.tsx using fakemui)
|
||||
- [x] Add page navigation controls (first, last, prev, next buttons)
|
||||
- [x] Add items-per-page selector (ItemsPerPageSelector.tsx)
|
||||
- [x] Add pagination info display (PaginationInfo.tsx)
|
||||
- [x] Write unit tests for pagination components (25 tests)
|
||||
- [ ] Update list views to use pagination (pending integration)
|
||||
- [ ] Write E2E tests for pagination UI
|
||||
|
||||
##### 2.5 Filtering and Sorting ✅ COMPLETE
|
||||
**Status:** ✅ All filtering and sorting utilities implemented (January 8, 2026)
|
||||
@@ -699,17 +701,19 @@ All criteria met ✅
|
||||
- [x] Field name validation for security
|
||||
- [x] Write comprehensive tests for sorting (included in 36 tests)
|
||||
|
||||
##### 2.6 Authentication Middleware
|
||||
**Target:** Week 5 of Q1 2026
|
||||
##### 2.6 Authentication Middleware ✅ COMPLETE
|
||||
**Status:** ✅ All authentication middleware implemented (January 8, 2026)
|
||||
|
||||
- [ ] **API Authentication**
|
||||
- [ ] Create auth middleware for API routes
|
||||
- [ ] Validate session tokens from cookies
|
||||
- [ ] Check user permission levels
|
||||
- [ ] Return 401 for unauthenticated requests
|
||||
- [ ] Return 403 for insufficient permissions
|
||||
- [ ] Add auth bypass for public endpoints
|
||||
- [ ] Write tests for auth middleware (15+ test cases)
|
||||
- [x] **API Authentication** ✅ COMPLETE
|
||||
- [x] Create auth middleware for API routes (auth-middleware.ts)
|
||||
- [x] Validate session tokens from cookies via getCurrentUser()
|
||||
- [x] Check user permission levels (0-5 scale)
|
||||
- [x] Return 401 for unauthenticated requests
|
||||
- [x] Return 403 for insufficient permissions
|
||||
- [x] Add auth bypass for public endpoints (allowPublic option)
|
||||
- [x] Support custom permission checks
|
||||
- [x] Provide requireAuth helper for simplified usage
|
||||
- [x] Write tests for auth middleware (21 test cases - exceeded target)
|
||||
|
||||
##### 2.7 Rate Limiting
|
||||
**Target:** Week 5-6 of Q1 2026
|
||||
@@ -738,14 +742,15 @@ All criteria met ✅
|
||||
|
||||
#### Testing Requirements
|
||||
|
||||
**Unit Tests:** Target 150+ new tests - ✅ **EXCEEDED (148 tests implemented)**
|
||||
**Unit Tests:** Target 150+ new tests - ✅ **EXCEEDED (194 tests implemented)**
|
||||
- API route handlers: 50 tests ✅ Complete
|
||||
- API client functions: 29 tests ✅ Complete
|
||||
- Retry utilities: 38 tests ✅ Complete
|
||||
- Validation utilities: 39 tests ✅ Complete
|
||||
- Pagination utilities: 35 tests ✅ Complete
|
||||
- Filtering/sorting utilities: 36 tests ✅ Complete
|
||||
- Auth middleware: 15 tests 🔄 Pending
|
||||
- Pagination components: 25 tests ✅ Complete
|
||||
- Auth middleware: 21 tests ✅ Complete
|
||||
- Rate limiting: 8 tests 🔄 Pending
|
||||
- Error handling: 20 tests 🔄 Pending
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
"@aws-sdk/client-s3": "^3.958.0",
|
||||
"@aws-sdk/lib-storage": "^3.958.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.958.0",
|
||||
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"prisma": "^7.2.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
|
||||
@@ -1,23 +1,53 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||
import { PrismaAdapterDialect, type PrismaAdapterOptions, type PrismaContext } from './types'
|
||||
|
||||
export function createPrismaContext(
|
||||
databaseUrl?: string,
|
||||
options?: PrismaAdapterOptions
|
||||
): PrismaContext {
|
||||
console.log('[DBAL Prisma] Creating Prisma context')
|
||||
console.log('[DBAL Prisma] Database URL parameter:', databaseUrl)
|
||||
console.log('[DBAL Prisma] Options:', options)
|
||||
|
||||
const inferredDialect = options?.dialect ?? inferDialectFromUrl(databaseUrl)
|
||||
const prisma = new PrismaClient(
|
||||
databaseUrl
|
||||
? {
|
||||
datasources: { db: { url: databaseUrl } },
|
||||
} as any
|
||||
: undefined
|
||||
)
|
||||
console.log('[DBAL Prisma] Inferred dialect:', inferredDialect)
|
||||
|
||||
let prisma: PrismaClient
|
||||
|
||||
// For SQLite (or when dialect cannot be inferred), we need to use the driver adapter
|
||||
if (inferredDialect === 'sqlite' || !databaseUrl || inferredDialect === undefined) {
|
||||
// Use relative path as fallback
|
||||
const fallbackUrl = 'file:../../prisma/prisma/dev.db'
|
||||
const finalUrl = databaseUrl || fallbackUrl
|
||||
|
||||
// Ensure URL has file: prefix for SQLite
|
||||
const sqliteUrl = finalUrl.startsWith('file:') ? finalUrl : `file:${finalUrl}`
|
||||
|
||||
console.log('[DBAL Prisma] Using SQLite URL:', sqliteUrl)
|
||||
|
||||
try {
|
||||
// PrismaBetterSqlite3 is a factory that expects { url: string } config
|
||||
const adapter = new PrismaBetterSqlite3({ url: sqliteUrl })
|
||||
console.log('[DBAL Prisma] Adapter factory created successfully')
|
||||
|
||||
prisma = new PrismaClient({ adapter } as any)
|
||||
console.log('[DBAL Prisma] PrismaClient created successfully')
|
||||
} catch (error) {
|
||||
console.error('[DBAL Prisma] Error creating Prisma client:', error)
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// For PostgreSQL/MySQL with explicit connection strings
|
||||
// Note: Prisma 7 removed datasources config, so this may not work
|
||||
// Consider using adapters for all database types
|
||||
throw new Error(`Prisma 7 requires adapters. Unsupported database dialect: ${inferredDialect}. Please use SQLite or implement adapters for other databases.`)
|
||||
}
|
||||
|
||||
return {
|
||||
prisma,
|
||||
queryTimeout: options?.queryTimeout ?? 30000,
|
||||
dialect: inferredDialect ?? 'generic'
|
||||
dialect: inferredDialect ?? 'sqlite'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
545
docs/playwright-tests-investigation.md
Normal file
545
docs/playwright-tests-investigation.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Playwright Tests Investigation - Progress Update
|
||||
|
||||
## Problem Statement
|
||||
Do playwright tests actually work?
|
||||
|
||||
## Answer: **PARTIAL PROGRESS** - Application starts but database adapter issues prevent full functionality
|
||||
|
||||
### Latest Status (After Commit 7a1b44b)
|
||||
|
||||
**✅ Application Starts Successfully**
|
||||
The Next.js dev server now starts without crashing and responds on http://localhost:3000
|
||||
|
||||
**⚠️ Pages Return 500 Errors**
|
||||
While the server runs, database operations fail causing pages to return HTTP 500 errors
|
||||
|
||||
**🔍 Root Cause**
|
||||
Prisma adapter errors logged as warnings:
|
||||
```
|
||||
prisma:error: Cannot read properties of undefined (reading 'replace')
|
||||
clientVersion: '7.2.0', digest: '1318283539'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Work Completed
|
||||
|
||||
### 1. Test Infrastructure Analysis ✅
|
||||
**Finding**: Playwright tests are correctly configured
|
||||
- Test files are well-written
|
||||
- Configuration in `e2e/playwright.config.ts` is proper
|
||||
- Browsers can be installed successfully
|
||||
|
||||
### 2. Prisma Client Generation Fix ✅
|
||||
**Commit**: 651083e
|
||||
**Change**: Updated `e2e/playwright.config.ts` webServer command:
|
||||
```typescript
|
||||
command: 'npm run db:generate && npm run dev'
|
||||
```
|
||||
**Result**: Prisma client now generates automatically before dev server starts
|
||||
|
||||
### 3. Next.js Prisma Adapter Implementation ✅
|
||||
**Commits**: 9f37692, 878f06b
|
||||
**Changes**:
|
||||
- Created `createProductionPrisma()` function in `frontends/nextjs/src/lib/config/prisma.ts`
|
||||
- Uses `better-sqlite3` with `@prisma/adapter-better-sqlite3`
|
||||
- Handles DATABASE_URL environment variable with fallback
|
||||
- Fixed undefined DATABASE_URL error handling
|
||||
|
||||
### 4. DBAL Prisma Adapter Implementation ✅
|
||||
**Commits**: f19d044, 7a1b44b
|
||||
**Changes**:
|
||||
- Installed `@prisma/adapter-better-sqlite3` and `better-sqlite3` in DBAL
|
||||
- Modified `dbal/development/src/adapters/prisma/context.ts`
|
||||
- Added SQLite dialect detection and adapter initialization
|
||||
- **Fixed fallback logic** to use SQLite adapter when dialect is undefined
|
||||
- Prevents attempting to use unsupported Prisma 6 syntax for unknown databases
|
||||
|
||||
### 5. Documentation Updates ✅
|
||||
**Files Created/Modified**:
|
||||
- `docs/playwright-tests-investigation.md` - Complete investigation report
|
||||
- `README.md` - Updated E2E test instructions with prerequisites
|
||||
|
||||
---
|
||||
|
||||
## Current Status: **IMPROVED BUT BLOCKED**
|
||||
|
||||
### Progress Made
|
||||
1. ✅ Application **starts successfully** (no longer crashes on startup)
|
||||
2. ✅ Server **stays running** and responds to requests
|
||||
3. ✅ Prisma errors are **logged but non-fatal**
|
||||
|
||||
### Remaining Issue
|
||||
**Database Operations Fail**:
|
||||
- Prisma adapter errors prevent database queries from completing
|
||||
- Pages return HTTP 500 Internal Server Error
|
||||
- Error originates from Prisma's internal adapter code
|
||||
- Suggests configuration mismatch between Prisma client and adapter setup
|
||||
|
||||
### Error Analysis
|
||||
The error `"Cannot read properties of undefined (reading 'replace')"` occurs:
|
||||
- Inside Prisma's generated client code ("ignore-listed frames")
|
||||
- When attempting database operations
|
||||
- Consistently with same digest: 1318283539
|
||||
- Multiple times per page request
|
||||
|
||||
### Possible Causes
|
||||
1. **Adapter Configuration Mismatch**: The PrismaClient and adapter may not be fully compatible
|
||||
2. **Database Path Issue**: The SQLite database path may not be resolving correctly
|
||||
3. **Missing Configuration**: Prisma 7 may require additional configuration not yet provided
|
||||
4. **Multiple Client Instances**: Different parts of the code may be creating incompatible Prisma clients
|
||||
|
||||
---
|
||||
|
||||
## Testing Status
|
||||
|
||||
### What Works
|
||||
- ✅ Dependencies install successfully
|
||||
- ✅ Playwright browsers install successfully
|
||||
- ✅ Prisma client generates successfully
|
||||
- ✅ Dev server command starts successfully
|
||||
- ✅ Server stays running and responds to HTTP requests
|
||||
- ✅ No TypeScript compilation errors
|
||||
|
||||
### What Doesn't Work
|
||||
- ❌ Database operations fail with adapter errors
|
||||
- ❌ Pages return HTTP 500 errors
|
||||
- ❌ Playwright tests cannot complete due to 500 errors
|
||||
|
||||
---
|
||||
|
||||
## Next Steps for Resolution
|
||||
|
||||
### Immediate Investigation Needed
|
||||
1. **Verify SQLite Database**
|
||||
- Confirm `prisma/prisma/dev.db` exists and is accessible
|
||||
- Check file permissions
|
||||
- Test direct database connection with better-sqlite3
|
||||
|
||||
2. **Debug Adapter Initialization**
|
||||
- Add logging to see what parameters are being passed to adapters
|
||||
- Verify DATABASE_URL is being read correctly
|
||||
- Check if database path resolution is working
|
||||
|
||||
3. **Test Prisma Client Directly**
|
||||
- Create a simple script to test PrismaClient with adapter
|
||||
- Verify basic CRUD operations work outside Next.js
|
||||
- Isolate whether issue is in Prisma setup or Next.js integration
|
||||
|
||||
4. **Review Prisma 7 Migration**
|
||||
- Check if additional Prisma 7 configuration files are needed
|
||||
- Review Prisma 7 changelog for SQLite adapter requirements
|
||||
- Consider if schema needs updates for Prisma 7 compatibility
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Configuration
|
||||
- `e2e/playwright.config.ts` - Added Prisma generation to webServer
|
||||
- `prisma/schema.prisma` - No changes (using adapter approach)
|
||||
|
||||
### Application Code
|
||||
- `frontends/nextjs/src/lib/config/prisma.ts` - Added SQLite adapter for production
|
||||
- `dbal/development/src/adapters/prisma/context.ts` - Added SQLite adapter with fallback
|
||||
|
||||
### Dependencies
|
||||
- `dbal/development/package.json` - Added SQLite adapter packages
|
||||
|
||||
### Documentation
|
||||
- `README.md` - Updated E2E testing instructions
|
||||
- `docs/playwright-tests-investigation.md` - This comprehensive report
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Significant Progress Made**: The application now starts and runs, which is a major improvement from the initial state where it crashed immediately.
|
||||
|
||||
**Remaining Challenge**: Database operations fail due to Prisma adapter configuration issues. The errors suggest a deeper compatibility problem between Prisma 7's adapter requirements and how the application is configured.
|
||||
|
||||
**Path Forward**: This requires debugging the Prisma adapter initialization to understand why the adapter is receiving undefined values or malformed parameters. Once database operations work, the Playwright test infrastructure is ready to run.
|
||||
|
||||
---
|
||||
|
||||
## Test Execution Commands
|
||||
|
||||
Once the database issue is resolved:
|
||||
|
||||
```bash
|
||||
# Prerequisites
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
|
||||
# Run tests (Prisma client generation now automatic)
|
||||
npm run test:e2e
|
||||
npm run test:e2e:ui # With Playwright UI
|
||||
npm run test:e2e:headed # With visible browser
|
||||
npm run test:e2e:debug # Debug mode
|
||||
```
|
||||
|
||||
### Root Cause Identified
|
||||
**Prisma 7 Breaking Changes**: Prisma 7 removed support for `url` in datasource blocks. Applications must now use:
|
||||
- Driver adapters (e.g., `@prisma/adapter-better-sqlite3` for SQLite), OR
|
||||
- Prisma Accelerate with `accelerateUrl`
|
||||
|
||||
This affects the entire MetaBuilder codebase which uses SQLite.
|
||||
|
||||
---
|
||||
|
||||
## Work Completed
|
||||
|
||||
### 1. Test Infrastructure Analysis ✅
|
||||
**Finding**: Playwright tests are correctly configured
|
||||
- Test files are well-written
|
||||
- Configuration in `e2e/playwright.config.ts` is proper
|
||||
- Browsers can be installed successfully
|
||||
|
||||
### 2. Prisma Client Generation Fix ✅
|
||||
**Commit**: 651083e
|
||||
**Change**: Updated `e2e/playwright.config.ts` webServer command:
|
||||
```typescript
|
||||
command: 'npm run db:generate && npm run dev'
|
||||
```
|
||||
**Result**: Prisma client now generates automatically before dev server starts
|
||||
|
||||
### 3. Next.js Prisma Adapter Implementation ✅
|
||||
**Commits**: 9f37692, 878f06b
|
||||
**Changes**:
|
||||
- Created `createProductionPrisma()` function in `frontends/nextjs/src/lib/config/prisma.ts`
|
||||
- Uses `better-sqlite3` with `@prisma/adapter-better-sqlite3`
|
||||
- Handles DATABASE_URL environment variable with fallback
|
||||
- Fixed undefined DATABASE_URL error handling
|
||||
|
||||
### 4. DBAL Prisma Adapter Implementation ✅
|
||||
**Commit**: f19d044
|
||||
**Changes**:
|
||||
- Installed `@prisma/adapter-better-sqlite3` and `better-sqlite3` in DBAL
|
||||
- Modified `dbal/development/src/adapters/prisma/context.ts`
|
||||
- Added SQLite dialect detection and adapter initialization
|
||||
- Maintained compatibility with PostgreSQL/MySQL
|
||||
|
||||
### 5. Documentation Updates ✅
|
||||
**Files Created/Modified**:
|
||||
- `docs/playwright-tests-investigation.md` - Complete investigation report
|
||||
- `README.md` - Updated E2E test instructions with prerequisites
|
||||
|
||||
---
|
||||
|
||||
## Current Status: **BLOCKED**
|
||||
|
||||
### Remaining Issue
|
||||
Application still fails to start with error:
|
||||
```
|
||||
TypeError: Cannot read properties of undefined (reading 'replace')
|
||||
at ignore-listed frames {
|
||||
clientVersion: '7.2.0',
|
||||
digest: '1521421786'
|
||||
}
|
||||
```
|
||||
|
||||
### Analysis
|
||||
1. The error originates from Prisma's internal code (ignore-listed frames)
|
||||
2. The digest changes between runs (1318283539 → 1521421786), suggesting multiple Prisma client initializations
|
||||
3. Despite adding adapters to both Next.js app and DBAL, an additional Prisma client may be created elsewhere
|
||||
4. The error occurs during application startup, preventing any pages from loading
|
||||
|
||||
### Possible Causes
|
||||
1. **Hidden Prisma Client**: Another Prisma client initialization in the codebase not using adapters
|
||||
2. **Build Cache**: Next.js build cache may contain old Prisma client references
|
||||
3. **Import Order**: Prisma client might be imported before environment variables are loaded
|
||||
4. **Nested Dependencies**: A package dependency might be creating its own Prisma client
|
||||
|
||||
---
|
||||
|
||||
## Testing Attempted
|
||||
|
||||
### What Works
|
||||
- ✅ Dependencies install successfully
|
||||
- ✅ Playwright browsers install successfully
|
||||
- ✅ Prisma client generates successfully
|
||||
- ✅ Dev server command starts
|
||||
- ✅ No TypeScript compilation errors
|
||||
|
||||
### What Doesn't Work
|
||||
- ❌ Application crashes immediately on startup
|
||||
- ❌ Cannot load any pages (including homepage)
|
||||
- ❌ All Playwright tests fail due to application not starting
|
||||
|
||||
---
|
||||
|
||||
## Next Steps for Resolution
|
||||
|
||||
### Immediate Actions Needed
|
||||
1. **Search for All Prisma Client Instantiations**
|
||||
```bash
|
||||
grep -r "new PrismaClient" --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
2. **Check for Dynamic Imports**
|
||||
- Look for `import('@prisma/client')`
|
||||
- Check middleware, API routes, server components
|
||||
|
||||
3. **Review Build Output**
|
||||
- Examine `.next/` directory for cached Prisma references
|
||||
- Consider adding Prisma adapter to Next.js webpack config
|
||||
|
||||
4. **Test with Explicit Environment Variables**
|
||||
```bash
|
||||
DATABASE_URL="file:./prisma/prisma/dev.db" npm run dev
|
||||
```
|
||||
|
||||
### Long-term Solutions
|
||||
1. **Upgrade Path**: Consider if Prisma 6 would be more compatible
|
||||
2. **Alternative Approach**: Use Prisma Accelerate instead of driver adapters
|
||||
3. **Configuration File**: Create `prisma.config.ts` as mentioned in Prisma 7 docs
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Configuration
|
||||
- `e2e/playwright.config.ts` - Added Prisma generation to webServer
|
||||
- `prisma/schema.prisma` - Attempted URL addition (reverted due to Prisma 7 requirements)
|
||||
|
||||
### Application Code
|
||||
- `frontends/nextjs/src/lib/config/prisma.ts` - Added SQLite adapter for production
|
||||
- `dbal/development/src/adapters/prisma/context.ts` - Added SQLite adapter for DBAL
|
||||
|
||||
### Dependencies
|
||||
- `dbal/development/package.json` - Added SQLite adapter packages
|
||||
|
||||
### Documentation
|
||||
- `README.md` - Updated E2E testing instructions
|
||||
- `docs/playwright-tests-investigation.md` - This comprehensive report
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**The Playwright test infrastructure is properly configured and would work correctly** if the Next.js application could start. The blocking issue is a deeper application architecture problem related to Prisma 7's breaking changes that affects database connectivity throughout the codebase.
|
||||
|
||||
**Recommendation**: This issue requires investigation by someone with deep knowledge of the MetaBuilder codebase to:
|
||||
1. Locate all Prisma client instantiations
|
||||
2. Ensure all use proper adapters for Prisma 7
|
||||
3. Verify the application can start successfully
|
||||
4. Then the existing Playwright tests should run correctly
|
||||
|
||||
---
|
||||
|
||||
## Test Execution Commands
|
||||
|
||||
Once the application issue is resolved:
|
||||
|
||||
```bash
|
||||
# Prerequisites
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
|
||||
# Run tests (Prisma client generation now automatic)
|
||||
npm run test:e2e
|
||||
npm run test:e2e:ui # With Playwright UI
|
||||
npm run test:e2e:headed # With visible browser
|
||||
npm run test:e2e:debug # Debug mode
|
||||
```
|
||||
|
||||
### Test Infrastructure
|
||||
- **Location**: `/e2e/` directory
|
||||
- **Configuration**: `e2e/playwright.config.ts`
|
||||
- **Test Files**:
|
||||
- `e2e/smoke.spec.ts` - Basic smoke tests
|
||||
- `e2e/login.spec.ts` - Authentication flow tests
|
||||
- `e2e/navigation.spec.ts` - Navigation and routing tests
|
||||
- `e2e/crud.spec.ts` - CRUD operation tests
|
||||
- Additional tests in subdirectories
|
||||
|
||||
### Configuration Analysis
|
||||
The Playwright configuration includes:
|
||||
- **Base URL**: `http://localhost:3000`
|
||||
- **Web Server**: Configured to run `npm run dev` before tests
|
||||
- **Timeout**: 300 seconds (5 minutes) for server startup
|
||||
- **Browser**: Chromium (Desktop Chrome)
|
||||
- **Retry Strategy**: 2 retries on CI, 0 locally
|
||||
|
||||
### Issues Found
|
||||
|
||||
#### 1. **Missing Prisma Client Generation** ❌
|
||||
**Severity**: CRITICAL - Blocks all tests
|
||||
|
||||
**Error**:
|
||||
```
|
||||
Error: Cannot find module '.prisma/client/default'
|
||||
```
|
||||
|
||||
**Root Cause**:
|
||||
- The web server attempts to start via `npm run dev`
|
||||
- Next.js application tries to load Prisma client
|
||||
- Prisma client has not been generated
|
||||
- Application cannot start, causing all tests to fail
|
||||
|
||||
**Impact**:
|
||||
- Dev server fails to start properly
|
||||
- All Playwright tests fail with "Cannot navigate to invalid URL"
|
||||
- Tests cannot connect to the application
|
||||
|
||||
**Solution Required**:
|
||||
```bash
|
||||
npm run db:generate # Generate Prisma client before running tests
|
||||
```
|
||||
|
||||
#### 2. **Prisma Client Configuration Error** ❌
|
||||
**Severity**: CRITICAL - Blocks all tests
|
||||
|
||||
**Error**:
|
||||
```
|
||||
Error [PrismaClientConstructorValidationError]: Using engine type "client" requires either "adapter" or "accelerateUrl" to be provided to PrismaClient constructor.
|
||||
```
|
||||
|
||||
**Root Cause**:
|
||||
- The Prisma schema is configured for SQLite
|
||||
- The application code at `frontends/nextjs/src/lib/config/prisma.ts:52` creates a PrismaClient instance
|
||||
- However, the Prisma client requires either:
|
||||
- A database adapter parameter, OR
|
||||
- An accelerateUrl for Prisma Accelerate
|
||||
- Neither is provided in the current configuration
|
||||
|
||||
**Impact**:
|
||||
- Dev server starts but crashes immediately when trying to access any page
|
||||
- Application cannot render any pages
|
||||
- All Playwright tests fail because the application is not functional
|
||||
|
||||
**This is an APPLICATION BUG, not a test configuration issue.**
|
||||
|
||||
### Test Execution Results
|
||||
|
||||
#### Without Prisma Client:
|
||||
```
|
||||
Running 7 tests using 2 workers
|
||||
|
||||
✘ All 7 tests FAILED
|
||||
|
||||
Failure reason: "Protocol error (Page.navigate): Cannot navigate to invalid URL"
|
||||
```
|
||||
|
||||
The webServer configuration tries to start but fails repeatedly with Prisma client errors, preventing Playwright from navigating to any pages.
|
||||
|
||||
### Recommendations
|
||||
|
||||
#### Immediate Fixes
|
||||
|
||||
1. **Add Prisma Generation Step**
|
||||
- Update test documentation to require `npm run db:generate`
|
||||
- Consider adding to playwright webServer command or globalSetup
|
||||
|
||||
2. **Update Playwright Configuration**
|
||||
```typescript
|
||||
webServer: {
|
||||
command: 'npm run db:generate && npm run dev', // Generate before starting
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 300 * 1000,
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add Global Setup Script**
|
||||
Create `e2e/global-setup.ts`:
|
||||
```typescript
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export default async function globalSetup() {
|
||||
console.log('Generating Prisma client...');
|
||||
execSync('npm run db:generate', { stdio: 'inherit' });
|
||||
console.log('Prisma client generated successfully');
|
||||
}
|
||||
```
|
||||
|
||||
4. **Document Prerequisites**
|
||||
Update testing documentation to clearly state:
|
||||
- Run `npm install` first
|
||||
- Run `npm run db:generate` before tests
|
||||
- Ensure database is accessible
|
||||
- Run `npx playwright install chromium` for browsers
|
||||
|
||||
#### Long-term Improvements
|
||||
|
||||
1. **Add Pre-Test Validation**
|
||||
- Check if Prisma client exists
|
||||
- Verify database connection
|
||||
- Confirm server can start
|
||||
- Provide helpful error messages
|
||||
|
||||
2. **CI/CD Integration**
|
||||
- Ensure CI pipeline runs `db:generate`
|
||||
- Set up test database properly
|
||||
- Cache Playwright browsers
|
||||
|
||||
3. **Better Error Messages**
|
||||
- Make Playwright config fail fast with clear messages
|
||||
- Add healthcheck endpoint to verify server readiness
|
||||
|
||||
### Conclusion
|
||||
|
||||
**Answer to "Do playwright tests actually work?"**
|
||||
|
||||
**NO** - The Playwright tests **cannot run successfully** due to two issues:
|
||||
|
||||
1. **FIXED**: Missing Prisma client generation - Now automatically handled by updated webServer command
|
||||
2. **BLOCKING**: Prisma Client configuration error in the application
|
||||
|
||||
#### Issue 1: Prisma Client Generation (FIXED ✅)
|
||||
- **Root Cause**: Prisma client not generated before dev server start
|
||||
- **Solution Applied**: Updated `e2e/playwright.config.ts` webServer command to `npm run db:generate && npm run dev`
|
||||
- **Status**: Fixed - Prisma client now generates automatically before tests
|
||||
|
||||
#### Issue 2: Prisma Client Configuration Error (BLOCKING ❌)
|
||||
- **Error**: `PrismaClientConstructorValidationError: Using engine type "client" requires either "adapter" or "accelerateUrl"`
|
||||
- **Location**: `frontends/nextjs/src/lib/config/prisma.ts:52`
|
||||
- **Root Cause**: The Prisma schema is configured to use SQLite, but the PrismaClient initialization at runtime requires either:
|
||||
- A database adapter to be provided, OR
|
||||
- An accelerateUrl for Prisma Accelerate
|
||||
- **Impact**: The Next.js application cannot start, preventing all E2E tests from running
|
||||
- **Status**: **This is an application bug**, not a testing infrastructure issue
|
||||
|
||||
#### Test Infrastructure Assessment
|
||||
|
||||
**✅ Working Correctly:**
|
||||
- Playwright installation and configuration
|
||||
- Browser (Chromium) installation
|
||||
- Test file structure and quality
|
||||
- WebServer configuration with auto-generation
|
||||
- Documentation and instructions
|
||||
|
||||
**❌ Blocked By:**
|
||||
- Application-level Prisma configuration error
|
||||
- Cannot be fixed in test configuration alone
|
||||
|
||||
**Next Steps to Enable Tests:**
|
||||
1. Fix the Prisma Client initialization in `frontends/nextjs/src/lib/config/prisma.ts`
|
||||
2. Ensure the application can start successfully with the SQLite database
|
||||
3. Once the application runs, the existing test infrastructure should work correctly
|
||||
|
||||
### Test Execution Commands (Corrected)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Install Playwright browsers
|
||||
npx playwright install chromium
|
||||
|
||||
# Generate Prisma client (CRITICAL STEP)
|
||||
npm run db:generate
|
||||
|
||||
# Run tests
|
||||
npm run test:e2e
|
||||
|
||||
# Or with UI
|
||||
npm run test:e2e:ui
|
||||
```
|
||||
|
||||
### Files Requiring Changes
|
||||
|
||||
1. `e2e/playwright.config.ts` - Add Prisma generation to webServer command or globalSetup
|
||||
2. `docs/TESTING_GUIDE.md` - Update prerequisites section
|
||||
3. `README.md` - Update E2E test instructions
|
||||
4. `.github/workflows/*.yml` - Ensure CI runs db:generate before tests
|
||||
|
||||
150
e2e/pagination.spec.ts
Normal file
150
e2e/pagination.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* E2E tests for pagination components
|
||||
*
|
||||
* These tests verify that pagination UI components render correctly
|
||||
* and handle user interactions properly.
|
||||
*/
|
||||
|
||||
test.describe('Pagination Components', () => {
|
||||
test.describe('PaginationControls', () => {
|
||||
test('should render pagination controls with page numbers', async ({ page }) => {
|
||||
// Navigate to a page that uses pagination
|
||||
await page.goto('/test-pagination') // This would be a test page with pagination
|
||||
|
||||
// Check that pagination controls are visible
|
||||
const pagination = page.locator('[aria-label="pagination navigation"]')
|
||||
await expect(pagination).toBeVisible()
|
||||
|
||||
// Check for navigation buttons
|
||||
await expect(page.locator('button[aria-label="Go to first page"]')).toBeVisible()
|
||||
await expect(page.locator('button[aria-label="Go to previous page"]')).toBeVisible()
|
||||
await expect(page.locator('button[aria-label="Go to next page"]')).toBeVisible()
|
||||
await expect(page.locator('button[aria-label="Go to last page"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should navigate between pages when clicking page numbers', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Click on page 2
|
||||
await page.click('button[aria-label="Go to page 2"]')
|
||||
|
||||
// Verify page 2 is now selected
|
||||
const page2Button = page.locator('button[aria-label="Go to page 2"]')
|
||||
await expect(page2Button).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
|
||||
test('should disable previous/first buttons on first page', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// On first page, previous and first buttons should be disabled
|
||||
await expect(page.locator('button[aria-label="Go to first page"]')).toBeDisabled()
|
||||
await expect(page.locator('button[aria-label="Go to previous page"]')).toBeDisabled()
|
||||
|
||||
// Next and last buttons should be enabled
|
||||
await expect(page.locator('button[aria-label="Go to next page"]')).not.toBeDisabled()
|
||||
await expect(page.locator('button[aria-label="Go to last page"]')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
test('should disable next/last buttons on last page', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Navigate to last page
|
||||
await page.click('button[aria-label="Go to last page"]')
|
||||
|
||||
// On last page, next and last buttons should be disabled
|
||||
await expect(page.locator('button[aria-label="Go to next page"]')).toBeDisabled()
|
||||
await expect(page.locator('button[aria-label="Go to last page"]')).toBeDisabled()
|
||||
|
||||
// Previous and first buttons should be enabled
|
||||
await expect(page.locator('button[aria-label="Go to previous page"]')).not.toBeDisabled()
|
||||
await expect(page.locator('button[aria-label="Go to first page"]')).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('ItemsPerPageSelector', () => {
|
||||
test('should display items per page selector', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Check that selector is visible
|
||||
const selector = page.locator('#items-per-page-select')
|
||||
await expect(selector).toBeVisible()
|
||||
})
|
||||
|
||||
test('should change page size when selecting different value', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Select 50 items per page
|
||||
await page.selectOption('#items-per-page-select', '50')
|
||||
|
||||
// Verify the selection changed (would need to check actual data display)
|
||||
const selector = page.locator('#items-per-page-select')
|
||||
await expect(selector).toHaveValue('50')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('PaginationInfo', () => {
|
||||
test('should display current page information', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Check that info text is displayed
|
||||
const infoText = page.locator('text=/Showing \\d+-\\d+ of \\d+ items/')
|
||||
await expect(infoText).toBeVisible()
|
||||
})
|
||||
|
||||
test('should update info when navigating pages', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// Get initial info text
|
||||
const initialInfo = await page.locator('text=/Showing \\d+-\\d+ of \\d+ items/').textContent()
|
||||
|
||||
// Navigate to page 2
|
||||
await page.click('button[aria-label="Go to page 2"]')
|
||||
|
||||
// Get updated info text
|
||||
const updatedInfo = await page.locator('text=/Showing \\d+-\\d+ of \\d+ items/').textContent()
|
||||
|
||||
// Verify info changed
|
||||
expect(updatedInfo).not.toBe(initialInfo)
|
||||
})
|
||||
|
||||
test('should display "No items found" when no data', async ({ page }) => {
|
||||
await page.goto('/test-pagination-empty') // Test page with no data
|
||||
|
||||
// Check for "No items found" message
|
||||
await expect(page.locator('text=No items found')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Complete pagination flow', () => {
|
||||
test('should handle complete pagination workflow', async ({ page }) => {
|
||||
await page.goto('/test-pagination')
|
||||
|
||||
// 1. Verify initial state
|
||||
await expect(page.locator('button[aria-label="Go to page 1"]')).toHaveAttribute('aria-current', 'page')
|
||||
await expect(page.locator('text=/Showing 1-\\d+ of \\d+ items/')).toBeVisible()
|
||||
|
||||
// 2. Change items per page
|
||||
await page.selectOption('#items-per-page-select', '50')
|
||||
|
||||
// 3. Navigate to different page
|
||||
await page.click('button[aria-label="Go to page 2"]')
|
||||
await expect(page.locator('button[aria-label="Go to page 2"]')).toHaveAttribute('aria-current', 'page')
|
||||
|
||||
// 4. Use next button
|
||||
await page.click('button[aria-label="Go to next page"]')
|
||||
await expect(page.locator('button[aria-label="Go to page 3"]')).toHaveAttribute('aria-current', 'page')
|
||||
|
||||
// 5. Go to last page
|
||||
await page.click('button[aria-label="Go to last page"]')
|
||||
|
||||
// 6. Use previous button
|
||||
await page.click('button[aria-label="Go to previous page"]')
|
||||
|
||||
// 7. Go back to first page
|
||||
await page.click('button[aria-label="Go to first page"]')
|
||||
await expect(page.locator('button[aria-label="Go to page 1"]')).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -34,7 +34,7 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
command: 'npm --prefix ../frontends/nextjs run db:generate && npm --prefix ../frontends/nextjs run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 300 * 1000,
|
||||
|
||||
9368
frontends/nextjs/package-lock.json
generated
9368
frontends/nextjs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { ItemsPerPageSelector } from './ItemsPerPageSelector'
|
||||
|
||||
describe('ItemsPerPageSelector', () => {
|
||||
it.each([
|
||||
{ value: 10, expectedValue: 10 },
|
||||
{ value: 20, expectedValue: 20 },
|
||||
{ value: 50, expectedValue: 50 },
|
||||
{ value: 100, expectedValue: 100 },
|
||||
])('should display selected value $value', ({ value, expectedValue }) => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ItemsPerPageSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// The select component should render without error
|
||||
const select = container.querySelector('select')
|
||||
expect(select).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call onChange when value is changed', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ItemsPerPageSelector
|
||||
value={20}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const select = container.querySelector('select')
|
||||
expect(select).toBeDefined()
|
||||
|
||||
// Simulate changing the select value
|
||||
if (select) {
|
||||
// Create a proper change event with a select element
|
||||
Object.defineProperty(select, 'value', {
|
||||
writable: true,
|
||||
value: '50'
|
||||
})
|
||||
fireEvent.change(select, { target: { value: '50' } })
|
||||
expect(onChange).toHaveBeenCalledWith(50)
|
||||
}
|
||||
})
|
||||
|
||||
it('should render custom options', () => {
|
||||
const onChange = vi.fn()
|
||||
const customOptions = [5, 15, 25]
|
||||
|
||||
const { container } = render(
|
||||
<ItemsPerPageSelector
|
||||
value={15}
|
||||
onChange={onChange}
|
||||
options={customOptions}
|
||||
/>
|
||||
)
|
||||
|
||||
const select = container.querySelector('select')
|
||||
expect(select).toBeDefined()
|
||||
|
||||
// Check all custom options are rendered as MenuItem children
|
||||
const menuItems = container.querySelectorAll('.menu-item')
|
||||
expect(menuItems.length).toBe(customOptions.length)
|
||||
})
|
||||
|
||||
it('should render custom label', () => {
|
||||
const onChange = vi.fn()
|
||||
const customLabel = 'Results per page'
|
||||
|
||||
render(
|
||||
<ItemsPerPageSelector
|
||||
value={20}
|
||||
onChange={onChange}
|
||||
label={customLabel}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByLabelText(customLabel)).toBeDefined()
|
||||
})
|
||||
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ItemsPerPageSelector
|
||||
value={20}
|
||||
onChange={onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
)
|
||||
|
||||
const select = container.querySelector('select')
|
||||
expect(select).toBeDefined()
|
||||
expect(select?.hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
|
||||
it('should render default options when none provided', () => {
|
||||
const onChange = vi.fn()
|
||||
const defaultOptions = [10, 20, 50, 100]
|
||||
|
||||
const { container } = render(
|
||||
<ItemsPerPageSelector
|
||||
value={20}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
|
||||
const select = container.querySelector('select')
|
||||
expect(select).toBeDefined()
|
||||
|
||||
// Check all default options are rendered as MenuItem children
|
||||
const menuItems = container.querySelectorAll('.menu-item')
|
||||
expect(menuItems.length).toBe(defaultOptions.length)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { FormControl, FormLabel, Select, MenuItem, Box } from '@/fakemui'
|
||||
|
||||
export interface ItemsPerPageSelectorProps {
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
options?: number[]
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS = [10, 20, 50, 100]
|
||||
|
||||
/**
|
||||
* Material-UI based items-per-page selector
|
||||
*
|
||||
* Allows users to select how many items to display per page
|
||||
* following MetaBuilder's design system using Material-UI components
|
||||
*/
|
||||
export function ItemsPerPageSelector({
|
||||
value,
|
||||
onChange,
|
||||
options = DEFAULT_OPTIONS,
|
||||
disabled = false,
|
||||
label = 'Items per page',
|
||||
}: ItemsPerPageSelectorProps) {
|
||||
const handleChange = (event: any) => {
|
||||
onChange(Number(event.target.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<FormControl disabled={disabled} sx={{ minWidth: 120 }}>
|
||||
<FormLabel htmlFor="items-per-page-select">{label}</FormLabel>
|
||||
<Select
|
||||
id="items-per-page-select"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
fontFamily: 'IBM Plex Sans, sans-serif',
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { PaginationControls } from './PaginationControls'
|
||||
import type { PaginationMetadata } from '@/lib/api/pagination'
|
||||
|
||||
describe('PaginationControls', () => {
|
||||
const createMetadata = (overrides?: Partial<PaginationMetadata>): PaginationMetadata => ({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 100,
|
||||
totalPages: 5,
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'first page',
|
||||
metadata: createMetadata({ page: 1, hasPreviousPage: false }),
|
||||
expectedPage: 1,
|
||||
},
|
||||
{
|
||||
name: 'middle page',
|
||||
metadata: createMetadata({ page: 3, hasPreviousPage: true, hasNextPage: true }),
|
||||
expectedPage: 3,
|
||||
},
|
||||
{
|
||||
name: 'last page',
|
||||
metadata: createMetadata({ page: 5, totalPages: 5, hasNextPage: false }),
|
||||
expectedPage: 5,
|
||||
},
|
||||
])('should render pagination for $name', ({ metadata, expectedPage }) => {
|
||||
const onPageChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PaginationControls
|
||||
metadata={metadata}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// Check that the current page button exists (fakemui uses "Go to page X" aria-label)
|
||||
const currentPageButton = screen.getByRole('button', { name: `Go to page ${expectedPage}`, current: 'page' })
|
||||
expect(currentPageButton).toBeDefined()
|
||||
})
|
||||
|
||||
it('should call onPageChange when page is clicked', () => {
|
||||
const onPageChange = vi.fn()
|
||||
const metadata = createMetadata()
|
||||
|
||||
render(
|
||||
<PaginationControls
|
||||
metadata={metadata}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// Click page 2 (fakemui uses "Go to page X" aria-label)
|
||||
const page2Button = screen.getByRole('button', { name: 'Go to page 2' })
|
||||
fireEvent.click(page2Button)
|
||||
|
||||
expect(onPageChange).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
it('should disable pagination when disabled prop is true', () => {
|
||||
const onPageChange = vi.fn()
|
||||
const metadata = createMetadata()
|
||||
|
||||
render(
|
||||
<PaginationControls
|
||||
metadata={metadata}
|
||||
onPageChange={onPageChange}
|
||||
disabled={true}
|
||||
/>
|
||||
)
|
||||
|
||||
// All buttons should be disabled (check disabled attribute)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
buttons.forEach(button => {
|
||||
expect(button.hasAttribute('disabled')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ size: 'small' as const },
|
||||
{ size: 'medium' as const },
|
||||
{ size: 'large' as const },
|
||||
])('should render with $size size', ({ size }) => {
|
||||
const onPageChange = vi.fn()
|
||||
const metadata = createMetadata()
|
||||
|
||||
const { container } = render(
|
||||
<PaginationControls
|
||||
metadata={metadata}
|
||||
onPageChange={onPageChange}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
|
||||
// Check that component renders without error
|
||||
expect(container.querySelector('.MuiPagination-root')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should render first and last buttons', () => {
|
||||
const onPageChange = vi.fn()
|
||||
const metadata = createMetadata({ page: 3, totalPages: 5 })
|
||||
|
||||
render(
|
||||
<PaginationControls
|
||||
metadata={metadata}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
)
|
||||
|
||||
// Should have first and last navigation buttons
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBeGreaterThan(5) // More than just page numbers
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { Pagination, Box } from '@/fakemui'
|
||||
import type { PaginationMetadata } from '@/lib/api/pagination'
|
||||
|
||||
export interface PaginationControlsProps {
|
||||
metadata: PaginationMetadata
|
||||
onPageChange: (page: number) => void
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Material-UI based pagination controls
|
||||
*
|
||||
* Displays page navigation with previous/next buttons and page numbers
|
||||
* following MetaBuilder's design system using Material-UI components
|
||||
*/
|
||||
export function PaginationControls({
|
||||
metadata,
|
||||
onPageChange,
|
||||
size = 'medium',
|
||||
disabled = false,
|
||||
}: PaginationControlsProps) {
|
||||
const handleChange = (page: number) => {
|
||||
onPageChange(page)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
py: 2,
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
count={metadata.totalPages}
|
||||
page={metadata.page}
|
||||
onChange={handleChange}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
showFirstButton
|
||||
showLastButton
|
||||
color="primary"
|
||||
sx={{
|
||||
'& .MuiPaginationItem-root': {
|
||||
fontFamily: 'IBM Plex Sans, sans-serif',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { PaginationInfo } from './PaginationInfo'
|
||||
import type { PaginationMetadata } from '@/lib/api/pagination'
|
||||
|
||||
describe('PaginationInfo', () => {
|
||||
const createMetadata = (overrides?: Partial<PaginationMetadata>): PaginationMetadata => ({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: 100,
|
||||
totalPages: 5,
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'first page of many',
|
||||
metadata: createMetadata({ page: 1, limit: 20, total: 100 }),
|
||||
expectedText: 'Showing 1-20 of 100 items',
|
||||
},
|
||||
{
|
||||
name: 'middle page',
|
||||
metadata: createMetadata({ page: 3, limit: 20, total: 100 }),
|
||||
expectedText: 'Showing 41-60 of 100 items',
|
||||
},
|
||||
{
|
||||
name: 'last page with partial results',
|
||||
metadata: createMetadata({ page: 5, limit: 20, total: 85 }),
|
||||
expectedText: 'Showing 81-85 of 85 items',
|
||||
},
|
||||
{
|
||||
name: 'single page with few items',
|
||||
metadata: createMetadata({ page: 1, limit: 20, total: 5 }),
|
||||
expectedText: 'Showing 1-5 of 5 items',
|
||||
},
|
||||
{
|
||||
name: 'page with 1 item',
|
||||
metadata: createMetadata({ page: 1, limit: 20, total: 1 }),
|
||||
expectedText: 'Showing 1-1 of 1 items',
|
||||
},
|
||||
])('should display correct text for $name', ({ metadata, expectedText }) => {
|
||||
render(<PaginationInfo metadata={metadata} />)
|
||||
|
||||
expect(screen.getByText(expectedText)).toBeDefined()
|
||||
})
|
||||
|
||||
it('should display "No items found" when total is 0', () => {
|
||||
const metadata = createMetadata({ page: 1, limit: 20, total: 0, totalPages: 0 })
|
||||
|
||||
render(<PaginationInfo metadata={metadata} />)
|
||||
|
||||
expect(screen.getByText('No items found')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle different page sizes correctly', () => {
|
||||
const testCases = [
|
||||
{ page: 1, limit: 10, total: 50, expected: 'Showing 1-10 of 50 items' },
|
||||
{ page: 2, limit: 10, total: 50, expected: 'Showing 11-20 of 50 items' },
|
||||
{ page: 1, limit: 50, total: 100, expected: 'Showing 1-50 of 100 items' },
|
||||
{ page: 3, limit: 50, total: 125, expected: 'Showing 101-125 of 125 items' },
|
||||
]
|
||||
|
||||
testCases.forEach(({ page, limit, total, expected }) => {
|
||||
const metadata = createMetadata({ page, limit, total })
|
||||
const { unmount } = render(<PaginationInfo metadata={metadata} />)
|
||||
|
||||
expect(screen.getByText(expected)).toBeDefined()
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { Typography } from '@/fakemui'
|
||||
import type { PaginationMetadata } from '@/lib/api/pagination'
|
||||
|
||||
export interface PaginationInfoProps {
|
||||
metadata: PaginationMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Display pagination information text
|
||||
*
|
||||
* Shows "Showing X-Y of Z items" information
|
||||
* following MetaBuilder's design system using Material-UI components
|
||||
*/
|
||||
export function PaginationInfo({ metadata }: PaginationInfoProps) {
|
||||
const start = (metadata.page - 1) * metadata.limit + 1
|
||||
const end = Math.min(metadata.page * metadata.limit, metadata.total)
|
||||
|
||||
if (metadata.total === 0) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontFamily: 'IBM Plex Sans, sans-serif' }}
|
||||
>
|
||||
No items found
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontFamily: 'IBM Plex Sans, sans-serif' }}
|
||||
>
|
||||
Showing {start}-{end} of {metadata.total} items
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
14
frontends/nextjs/src/components/pagination/index.ts
Normal file
14
frontends/nextjs/src/components/pagination/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Pagination components for MetaBuilder
|
||||
*
|
||||
* Material-UI based pagination controls following MetaBuilder's design system
|
||||
*/
|
||||
|
||||
export { PaginationControls } from './PaginationControls'
|
||||
export type { PaginationControlsProps } from './PaginationControls'
|
||||
|
||||
export { ItemsPerPageSelector } from './ItemsPerPageSelector'
|
||||
export type { ItemsPerPageSelectorProps } from './ItemsPerPageSelector'
|
||||
|
||||
export { PaginationInfo } from './PaginationInfo'
|
||||
export type { PaginationInfoProps } from './PaginationInfo'
|
||||
@@ -9,11 +9,9 @@
|
||||
// these assignments/calls are safe for runtime but look unsafe to the linter.
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
|
||||
import Database from 'better-sqlite3'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
prismaTestDb: Database | undefined
|
||||
}
|
||||
|
||||
const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true'
|
||||
@@ -37,21 +35,47 @@ const createMockPrisma = (): PrismaClient => {
|
||||
}
|
||||
|
||||
const createIntegrationPrisma = (): PrismaClient => {
|
||||
if (globalForPrisma.prismaTestDb === undefined) {
|
||||
globalForPrisma.prismaTestDb = new Database(':memory:')
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const adapter = new PrismaBetterSqlite3(globalForPrisma.prismaTestDb)
|
||||
// For integration tests, use in-memory database via adapter factory
|
||||
const adapter = new PrismaBetterSqlite3({ url: ':memory:' })
|
||||
return new PrismaClient({ adapter })
|
||||
}
|
||||
|
||||
const createProductionPrisma = (): PrismaClient => {
|
||||
// CRITICAL: Validate DATABASE_URL is set and properly formatted
|
||||
const databaseUrl = process.env.DATABASE_URL || 'file:../../prisma/prisma/dev.db'
|
||||
|
||||
console.log('[Prisma] Creating production Prisma client')
|
||||
console.log('[Prisma] DATABASE_URL from env:', process.env.DATABASE_URL)
|
||||
console.log('[Prisma] Using database URL:', databaseUrl)
|
||||
|
||||
// Validate URL format for SQLite
|
||||
if (!databaseUrl.startsWith('file:')) {
|
||||
throw new Error(`[Prisma] Invalid DATABASE_URL format: "${databaseUrl}". SQLite requires "file:" prefix.`)
|
||||
}
|
||||
|
||||
try {
|
||||
// For Prisma 7, PrismaBetterSqlite3 is a FACTORY that takes config with url, not a client instance
|
||||
const adapter = new PrismaBetterSqlite3({ url: databaseUrl })
|
||||
console.log('[Prisma] Adapter factory created successfully')
|
||||
|
||||
const client = new PrismaClient({
|
||||
adapter,
|
||||
log: process.env.NODE_ENV === 'development' ? ['error', 'warn', 'query'] : ['error'],
|
||||
})
|
||||
console.log('[Prisma] PrismaClient created successfully')
|
||||
|
||||
return client
|
||||
} catch (error) {
|
||||
console.error('[Prisma] Error creating Prisma client:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
(isTestEnv
|
||||
? (isIntegrationTest ? createIntegrationPrisma() : createMockPrisma())
|
||||
: new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
|
||||
}))
|
||||
: createProductionPrisma())
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && (!isTestEnv || isIntegrationTest)) {
|
||||
globalForPrisma.prisma = prisma
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('getErrorLogs', () => {
|
||||
// Verify list was called with correct options
|
||||
expect(mockList).toHaveBeenCalledWith('ErrorLog', {
|
||||
filter: expectedFilter,
|
||||
orderBy: { timestamp: 'desc' },
|
||||
orderBy: [{ timestamp: 'desc' }],
|
||||
take: options?.limit ?? undefined,
|
||||
})
|
||||
expect(result).toHaveLength(dbData.length)
|
||||
|
||||
196
frontends/nextjs/src/lib/middleware/auth-middleware.test.ts
Normal file
196
frontends/nextjs/src/lib/middleware/auth-middleware.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { authenticate, requireAuth } from './auth-middleware'
|
||||
import type { CurrentUser } from '@/lib/auth/get-current-user'
|
||||
|
||||
// Mock the getCurrentUser function
|
||||
vi.mock('@/lib/auth/get-current-user', () => ({
|
||||
getCurrentUser: vi.fn(),
|
||||
}))
|
||||
|
||||
// Import mocked function for type safety
|
||||
import { getCurrentUser } from '@/lib/auth/get-current-user'
|
||||
|
||||
describe('auth-middleware', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const createMockUser = (overrides?: Partial<CurrentUser>): CurrentUser => ({
|
||||
id: 'user-123',
|
||||
email: 'user@example.com',
|
||||
username: 'testuser',
|
||||
role: 'user',
|
||||
level: 1,
|
||||
tenantId: 'tenant-1',
|
||||
passwordHash: 'hash',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockRequest = (): NextRequest => {
|
||||
return new NextRequest('http://localhost:3000/api/test')
|
||||
}
|
||||
|
||||
describe('authenticate', () => {
|
||||
describe('public access', () => {
|
||||
it.each([
|
||||
{ name: 'allows public access when allowPublic is true', allowPublic: true },
|
||||
])('should allow $name', async ({ allowPublic }) => {
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { allowPublic })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.user).toBeUndefined()
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('unauthenticated requests', () => {
|
||||
it.each([
|
||||
{ name: 'no user (null)', mockUser: null, expectedStatus: 401 },
|
||||
{ name: 'undefined user', mockUser: undefined, expectedStatus: 401 },
|
||||
])('should return 401 for $name', async ({ mockUser, expectedStatus }) => {
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser as any)
|
||||
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { minLevel: 0 })
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
|
||||
if (result.error) {
|
||||
expect(result.error.status).toBe(expectedStatus)
|
||||
const body = await result.error.json()
|
||||
expect(body.error).toBe('Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('permission level checks', () => {
|
||||
it.each([
|
||||
{ userLevel: 0, minLevel: 0, shouldPass: true, name: 'public user accessing public endpoint' },
|
||||
{ userLevel: 1, minLevel: 0, shouldPass: true, name: 'user accessing public endpoint' },
|
||||
{ userLevel: 1, minLevel: 1, shouldPass: true, name: 'user accessing user endpoint' },
|
||||
{ userLevel: 2, minLevel: 1, shouldPass: true, name: 'moderator accessing user endpoint' },
|
||||
{ userLevel: 3, minLevel: 3, shouldPass: true, name: 'admin accessing admin endpoint' },
|
||||
{ userLevel: 4, minLevel: 3, shouldPass: true, name: 'god accessing admin endpoint' },
|
||||
{ userLevel: 5, minLevel: 5, shouldPass: true, name: 'supergod accessing supergod endpoint' },
|
||||
{ userLevel: 0, minLevel: 1, shouldPass: false, name: 'public user accessing user endpoint' },
|
||||
{ userLevel: 1, minLevel: 2, shouldPass: false, name: 'user accessing moderator endpoint' },
|
||||
{ userLevel: 2, minLevel: 3, shouldPass: false, name: 'moderator accessing admin endpoint' },
|
||||
{ userLevel: 3, minLevel: 4, shouldPass: false, name: 'admin accessing god endpoint' },
|
||||
{ userLevel: 4, minLevel: 5, shouldPass: false, name: 'god accessing supergod endpoint' },
|
||||
])('should handle $name (level $userLevel, required $minLevel)', async ({ userLevel, minLevel, shouldPass }) => {
|
||||
const mockUser = createMockUser({ level: userLevel })
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
|
||||
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { minLevel })
|
||||
|
||||
if (shouldPass) {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.user).toEqual(mockUser)
|
||||
expect(result.error).toBeUndefined()
|
||||
} else {
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
|
||||
if (result.error) {
|
||||
expect(result.error.status).toBe(403)
|
||||
const body = await result.error.json()
|
||||
expect(body.error).toBe('Forbidden')
|
||||
expect(body.requiredLevel).toBe(minLevel)
|
||||
expect(body.userLevel).toBe(userLevel)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom permission checks', () => {
|
||||
it('should pass when custom check returns true', async () => {
|
||||
const mockUser = createMockUser()
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
|
||||
|
||||
const customCheck = vi.fn().mockReturnValue(true)
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { customCheck })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(customCheck).toHaveBeenCalledWith(mockUser)
|
||||
})
|
||||
|
||||
it('should fail when custom check returns false', async () => {
|
||||
const mockUser = createMockUser()
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
|
||||
|
||||
const customCheck = vi.fn().mockReturnValue(false)
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { customCheck })
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
|
||||
if (result.error) {
|
||||
expect(result.error.status).toBe(403)
|
||||
const body = await result.error.json()
|
||||
expect(body.error).toBe('Forbidden')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle getCurrentUser errors', async () => {
|
||||
vi.mocked(getCurrentUser).mockRejectedValue(new Error('Database error'))
|
||||
|
||||
const request = createMockRequest()
|
||||
const result = await authenticate(request, { minLevel: 1 })
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
|
||||
if (result.error) {
|
||||
expect(result.error.status).toBe(500)
|
||||
const body = await result.error.json()
|
||||
expect(body.error).toBe('Internal Server Error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('requireAuth', () => {
|
||||
it('should return user when authentication succeeds', async () => {
|
||||
const mockUser = createMockUser()
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
|
||||
|
||||
const request = createMockRequest()
|
||||
const user = await requireAuth(request, { minLevel: 1 })
|
||||
|
||||
expect(user).toEqual(mockUser)
|
||||
})
|
||||
|
||||
it('should throw error response when authentication fails', async () => {
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(null)
|
||||
|
||||
const request = createMockRequest()
|
||||
|
||||
await expect(
|
||||
requireAuth(request, { minLevel: 1 })
|
||||
).rejects.toBeDefined()
|
||||
})
|
||||
|
||||
it('should throw error response when permission check fails', async () => {
|
||||
const mockUser = createMockUser({ level: 1 })
|
||||
vi.mocked(getCurrentUser).mockResolvedValue(mockUser)
|
||||
|
||||
const request = createMockRequest()
|
||||
|
||||
await expect(
|
||||
requireAuth(request, { minLevel: 3 })
|
||||
).rejects.toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
170
frontends/nextjs/src/lib/middleware/auth-middleware.ts
Normal file
170
frontends/nextjs/src/lib/middleware/auth-middleware.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Authentication middleware for API routes
|
||||
*
|
||||
* Validates session tokens and checks permission levels for API endpoints.
|
||||
* Returns standardized error responses for unauthorized or forbidden requests.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getCurrentUser, type CurrentUser } from '@/lib/auth/get-current-user'
|
||||
|
||||
export interface AuthMiddlewareOptions {
|
||||
/**
|
||||
* Minimum permission level required (0-5)
|
||||
* 0 = Public, 1 = User, 2 = Moderator, 3 = Admin, 4 = God, 5 = Supergod
|
||||
*/
|
||||
minLevel?: number
|
||||
|
||||
/**
|
||||
* Allow public access (skip authentication)
|
||||
*/
|
||||
allowPublic?: boolean
|
||||
|
||||
/**
|
||||
* Custom permission check function
|
||||
* @returns true if access is allowed, false otherwise
|
||||
*/
|
||||
customCheck?: (user: CurrentUser | null) => boolean
|
||||
}
|
||||
|
||||
export interface AuthenticatedRequest extends NextRequest {
|
||||
user: CurrentUser
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication middleware result
|
||||
*/
|
||||
export interface AuthResult {
|
||||
/**
|
||||
* Whether authentication succeeded
|
||||
*/
|
||||
success: boolean
|
||||
|
||||
/**
|
||||
* Authenticated user (only present if success = true)
|
||||
*/
|
||||
user?: CurrentUser
|
||||
|
||||
/**
|
||||
* Error response (only present if success = false)
|
||||
*/
|
||||
error?: NextResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the request and check permissions
|
||||
*
|
||||
* @param request - Next.js request object
|
||||
* @param options - Authentication options
|
||||
* @returns Authentication result with user or error response
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In API route
|
||||
* const { success, user, error } = await authenticate(request, { minLevel: 1 })
|
||||
* if (!success) return error
|
||||
*
|
||||
* // Use authenticated user
|
||||
* const data = await getData(user.id)
|
||||
* ```
|
||||
*/
|
||||
export async function authenticate(
|
||||
request: NextRequest,
|
||||
options: AuthMiddlewareOptions = {}
|
||||
): Promise<AuthResult> {
|
||||
const { minLevel = 0, allowPublic = false, customCheck } = options
|
||||
|
||||
// Allow public endpoints
|
||||
if (allowPublic) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current user from session
|
||||
const user = await getCurrentUser()
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: NextResponse.json(
|
||||
{ error: 'Unauthorized', message: 'Authentication required' },
|
||||
{ status: 401 }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Check permission level
|
||||
if (user.level < minLevel) {
|
||||
return {
|
||||
success: false,
|
||||
error: NextResponse.json(
|
||||
{
|
||||
error: 'Forbidden',
|
||||
message: `Insufficient permissions. Required level: ${minLevel}, your level: ${user.level}`,
|
||||
requiredLevel: minLevel,
|
||||
userLevel: user.level,
|
||||
},
|
||||
{ status: 403 }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Run custom permission check if provided
|
||||
if (customCheck && !customCheck(user)) {
|
||||
return {
|
||||
success: false,
|
||||
error: NextResponse.json(
|
||||
{ error: 'Forbidden', message: 'Permission denied' },
|
||||
{ status: 403 }
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication successful
|
||||
return {
|
||||
success: true,
|
||||
user,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: NextResponse.json(
|
||||
{ error: 'Internal Server Error', message: 'Authentication failed' },
|
||||
{ status: 500 }
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication for an API route
|
||||
*
|
||||
* Simplified helper that throws an error response if authentication fails.
|
||||
* Use this when you want to handle authentication in a single line.
|
||||
*
|
||||
* @param request - Next.js request object
|
||||
* @param options - Authentication options
|
||||
* @returns Authenticated user
|
||||
* @throws NextResponse with error status if authentication fails
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In API route
|
||||
* const user = await requireAuth(request, { minLevel: 3 })
|
||||
* // If we get here, user is authenticated and has admin level
|
||||
* ```
|
||||
*/
|
||||
export async function requireAuth(
|
||||
request: NextRequest,
|
||||
options: AuthMiddlewareOptions = {}
|
||||
): Promise<CurrentUser> {
|
||||
const { success, user, error } = await authenticate(request, options)
|
||||
|
||||
if (!success || !user) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
10
frontends/nextjs/src/lib/middleware/index.ts
Normal file
10
frontends/nextjs/src/lib/middleware/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Middleware exports for MetaBuilder
|
||||
*/
|
||||
|
||||
export { authenticate, requireAuth } from './auth-middleware'
|
||||
export type {
|
||||
AuthMiddlewareOptions,
|
||||
AuthenticatedRequest,
|
||||
AuthResult,
|
||||
} from './auth-middleware'
|
||||
79
frontends/nextjs/test-prisma-adapter.cjs
Normal file
79
frontends/nextjs/test-prisma-adapter.cjs
Normal file
@@ -0,0 +1,79 @@
|
||||
// Test script to debug Prisma adapter initialization
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
console.log('Testing Prisma adapter initialization...\n');
|
||||
|
||||
// Test 1: Direct database path
|
||||
console.log('Test 1: Using direct path');
|
||||
try {
|
||||
const dbPath1 = './prisma/prisma/dev.db';
|
||||
console.log(` Database path: ${dbPath1}`);
|
||||
const db1 = new Database(dbPath1);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter1 = new PrismaBetterSqlite3(db1);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma1 = new PrismaClient({ adapter: adapter1 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
|
||||
// Try a simple query
|
||||
prisma1.user.findMany().then(users => {
|
||||
console.log(` ✓ Query successful, found ${users.length} users`);
|
||||
prisma1.$disconnect();
|
||||
}).catch(err => {
|
||||
console.log(` ✗ Query failed: ${err.message}`);
|
||||
prisma1.$disconnect();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
// Test 2: Using environment variable style path
|
||||
console.log('\nTest 2: Using file: prefix');
|
||||
try {
|
||||
const databaseUrl = 'file:./prisma/prisma/dev.db';
|
||||
const dbPath2 = databaseUrl.replace('file:', '');
|
||||
console.log(` Database URL: ${databaseUrl}`);
|
||||
console.log(` Resolved path: ${dbPath2}`);
|
||||
const db2 = new Database(dbPath2);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter2 = new PrismaBetterSqlite3(db2);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma2 = new PrismaClient({ adapter: adapter2 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
|
||||
// Try a simple query
|
||||
prisma2.user.count().then(count => {
|
||||
console.log(` ✓ Count query successful, ${count} users total`);
|
||||
prisma2.$disconnect();
|
||||
}).catch(err => {
|
||||
console.log(` ✗ Query failed: ${err.message}`);
|
||||
prisma2.$disconnect();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
// Test 3: Check what happens with undefined
|
||||
console.log('\nTest 3: Testing undefined handling');
|
||||
try {
|
||||
const databaseUrl = undefined;
|
||||
const dbPath3 = databaseUrl?.replace('file:', '') || './prisma/prisma/dev.db';
|
||||
console.log(` Database URL: ${databaseUrl}`);
|
||||
console.log(` Resolved path: ${dbPath3}`);
|
||||
const db3 = new Database(dbPath3);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter3 = new PrismaBetterSqlite3(db3);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma3 = new PrismaClient({ adapter: adapter3 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
prisma3.$disconnect();
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
console.log('\nAll tests completed!');
|
||||
Binary file not shown.
@@ -19,7 +19,7 @@
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
"@storybook/react": "^8.6.15",
|
||||
"@storybook/react": "^10.1.11",
|
||||
"@storybook/react-vite": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@types/react": "^18.3.18",
|
||||
|
||||
79
test-prisma-adapter.js
Normal file
79
test-prisma-adapter.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// Test script to debug Prisma adapter initialization
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
console.log('Testing Prisma adapter initialization...\n');
|
||||
|
||||
// Test 1: Direct database path
|
||||
console.log('Test 1: Using direct path');
|
||||
try {
|
||||
const dbPath1 = './prisma/prisma/dev.db';
|
||||
console.log(` Database path: ${dbPath1}`);
|
||||
const db1 = new Database(dbPath1);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter1 = new PrismaBetterSqlite3(db1);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma1 = new PrismaClient({ adapter: adapter1 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
|
||||
// Try a simple query
|
||||
prisma1.user.findMany().then(users => {
|
||||
console.log(` ✓ Query successful, found ${users.length} users`);
|
||||
prisma1.$disconnect();
|
||||
}).catch(err => {
|
||||
console.log(` ✗ Query failed: ${err.message}`);
|
||||
prisma1.$disconnect();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
// Test 2: Using environment variable style path
|
||||
console.log('\nTest 2: Using file: prefix');
|
||||
try {
|
||||
const databaseUrl = 'file:./prisma/prisma/dev.db';
|
||||
const dbPath2 = databaseUrl.replace('file:', '');
|
||||
console.log(` Database URL: ${databaseUrl}`);
|
||||
console.log(` Resolved path: ${dbPath2}`);
|
||||
const db2 = new Database(dbPath2);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter2 = new PrismaBetterSqlite3(db2);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma2 = new PrismaClient({ adapter: adapter2 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
|
||||
// Try a simple query
|
||||
prisma2.user.count().then(count => {
|
||||
console.log(` ✓ Count query successful, ${count} users total`);
|
||||
prisma2.$disconnect();
|
||||
}).catch(err => {
|
||||
console.log(` ✗ Query failed: ${err.message}`);
|
||||
prisma2.$disconnect();
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
// Test 3: Check what happens with undefined
|
||||
console.log('\nTest 3: Testing undefined handling');
|
||||
try {
|
||||
const databaseUrl = undefined;
|
||||
const dbPath3 = databaseUrl?.replace('file:', '') || './prisma/prisma/dev.db';
|
||||
console.log(` Database URL: ${databaseUrl}`);
|
||||
console.log(` Resolved path: ${dbPath3}`);
|
||||
const db3 = new Database(dbPath3);
|
||||
console.log(' ✓ Database opened successfully');
|
||||
const adapter3 = new PrismaBetterSqlite3(db3);
|
||||
console.log(' ✓ Adapter created successfully');
|
||||
const prisma3 = new PrismaClient({ adapter: adapter3 });
|
||||
console.log(' ✓ PrismaClient created successfully');
|
||||
prisma3.$disconnect();
|
||||
} catch (error) {
|
||||
console.log(` ✗ Error: ${error.message}`);
|
||||
console.log(` Stack: ${error.stack}`);
|
||||
}
|
||||
|
||||
console.log('\nAll tests completed!');
|
||||
Reference in New Issue
Block a user