diff --git a/backend/cmd/genhash/main.go b/backend/cmd/genhash/main.go new file mode 100644 index 0000000..59cc89c --- /dev/null +++ b/backend/cmd/genhash/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func main() { + password := "Admin@2025!" + pepper := "gohorse-pepper" + passwordWithPepper := password + pepper + + hash, err := bcrypt.GenerateFromPassword([]byte(passwordWithPepper), bcrypt.DefaultCost) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Printf("-- New hash for superadmin (password: Admin@2025!, pepper: gohorse-pepper)\n") + fmt.Printf("UPDATE users SET password_hash = '%s' WHERE identifier = 'superadmin';\n", string(hash)) +} diff --git a/backend/internal/infrastructure/auth/jwt_service_test.go b/backend/internal/infrastructure/auth/jwt_service_test.go index ecdfccb..f3790e4 100644 --- a/backend/internal/infrastructure/auth/jwt_service_test.go +++ b/backend/internal/infrastructure/auth/jwt_service_test.go @@ -207,3 +207,142 @@ func TestJWTService_NewJWTService(t *testing.T) { fmt.Printf("[TEST LOG] Service created: %v\n", service) assert.NotNil(t, service) } + +// ============ ADDITIONAL TESTS (10% expansion) ============ + +func TestJWTService_HashPassword_EmptyPassword(t *testing.T) { + fmt.Println("\n[TEST] === TestJWTService_HashPassword_EmptyPassword ===") + + os.Setenv("PASSWORD_PEPPER", "test-pepper") + defer os.Unsetenv("PASSWORD_PEPPER") + + service := auth.NewJWTService("secret", "issuer") + + t.Run("Empty password should still hash", func(t *testing.T) { + hash, err := service.HashPassword("") + fmt.Printf("[TEST LOG] Empty password hash: %s...\n", hash[:20]) + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + // Should verify with empty password + valid := service.VerifyPassword(hash, "") + assert.True(t, valid) + }) +} + +func TestJWTService_PepperConsistency(t *testing.T) { + fmt.Println("\n[TEST] === TestJWTService_PepperConsistency ===") + + t.Run("Same pepper produces verifiable hash", func(t *testing.T) { + os.Setenv("PASSWORD_PEPPER", "consistent-pepper") + service := auth.NewJWTService("secret", "issuer") + + password := "Admin@2025!" + hash1, _ := service.HashPassword(password) + + // Verify with same pepper + valid := service.VerifyPassword(hash1, password) + assert.True(t, valid, "Password should verify with same pepper") + os.Unsetenv("PASSWORD_PEPPER") + }) + + t.Run("Different peppers produce different hashes", func(t *testing.T) { + service := auth.NewJWTService("secret", "issuer") + + os.Setenv("PASSWORD_PEPPER", "pepper-1") + hash1, _ := service.HashPassword("password") + + os.Setenv("PASSWORD_PEPPER", "pepper-2") + hash2, _ := service.HashPassword("password") + + // Hashes should be different + assert.NotEqual(t, hash1, hash2, "Different peppers should produce different hashes") + os.Unsetenv("PASSWORD_PEPPER") + }) +} + +func TestJWTService_TokenClaims_Content(t *testing.T) { + fmt.Println("\n[TEST] === TestJWTService_TokenClaims_Content ===") + + service := auth.NewJWTService("secret", "issuer") + + t.Run("Token should contain all expected claims", func(t *testing.T) { + userID := "019b51a9-385f-7416-be8c-9960727531a3" + tenantID := "tenant-xyz" + roles := []string{"admin", "superadmin"} + + token, err := service.GenerateToken(userID, tenantID, roles) + assert.NoError(t, err) + + claims, err := service.ValidateToken(token) + assert.NoError(t, err) + + // Check all claims + assert.Equal(t, userID, claims["sub"]) + assert.Equal(t, tenantID, claims["tenant"]) + assert.Equal(t, "issuer", claims["iss"]) + assert.NotNil(t, claims["exp"]) + assert.NotNil(t, claims["iat"]) + + // Check roles + rolesFromClaims := claims["roles"].([]interface{}) + assert.Len(t, rolesFromClaims, 2) + }) +} + +func TestJWTService_LongPassword(t *testing.T) { + fmt.Println("\n[TEST] === TestJWTService_LongPassword ===") + + os.Setenv("PASSWORD_PEPPER", "test-pepper") + defer os.Unsetenv("PASSWORD_PEPPER") + + service := auth.NewJWTService("secret", "issuer") + + t.Run("Very long password should work", func(t *testing.T) { + // bcrypt has a 72 byte limit, test behavior + longPassword := "ThisIsAVeryLongPasswordThatExceeds72BytesWhichIsTheMaxForBcryptSoItWillBeTruncated123" + hash, err := service.HashPassword(longPassword) + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + valid := service.VerifyPassword(hash, longPassword) + assert.True(t, valid) + }) +} + +func TestJWTService_SpecialCharactersPassword(t *testing.T) { + fmt.Println("\n[TEST] === TestJWTService_SpecialCharactersPassword ===") + + os.Setenv("PASSWORD_PEPPER", "test-pepper") + defer os.Unsetenv("PASSWORD_PEPPER") + + service := auth.NewJWTService("secret", "issuer") + + specialPasswords := []string{ + "Admin@2025!", + "Pässwörd123", + "密码123", + "パスワード", + "🔐SecurePass!", + "test\nwith\nnewlines", + "tab\there", + } + + for _, password := range specialPasswords { + t.Run("Password: "+password[:min(10, len(password))], func(t *testing.T) { + hash, err := service.HashPassword(password) + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + valid := service.VerifyPassword(hash, password) + assert.True(t, valid, "Should verify password with special chars") + }) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/backend/migrations/010_seed_super_admin.sql b/backend/migrations/010_seed_super_admin.sql index adccb05..91bf4b7 100644 --- a/backend/migrations/010_seed_super_admin.sql +++ b/backend/migrations/010_seed_super_admin.sql @@ -1,7 +1,7 @@ -- Migration: Create Super Admin and System Company -- Description: Inserts the default System Company and Super Admin user. -- Uses unified tables (companies, users, user_roles) --- NOTE: This runs before migration 020 adds extra columns, so only use existing columns +-- HARDCODED: This is the official superadmin - password: Admin@2025! (with pepper: gohorse-pepper) -- 1. Insert System Company (for SuperAdmin context) INSERT INTO companies (name, slug, type, document, email, description, verified, active) @@ -16,17 +16,28 @@ VALUES ( true ) ON CONFLICT (slug) DO NOTHING; --- 2. Insert Super Admin User (using only columns from migration 001) --- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER. --- For development only. Use seeder-api for proper user creation with pepper. -INSERT INTO users (identifier, password_hash, role, full_name, active) +-- 2. Insert Super Admin User +-- Hash: bcrypt(Admin@2025! + gohorse-pepper) - HARDCODED for consistency +INSERT INTO users (identifier, password_hash, role, full_name, email, status, active) VALUES ( 'superadmin', - '$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder + '$2a$10$/AodyEEQtKCjdeNThEUFee6QE/KvEBTzi1AnqQ78nwavkT1XFnw/6', 'superadmin', 'Super Administrator', + 'admin@gohorsejobs.com', + 'active', true -) ON CONFLICT (identifier) DO NOTHING; +) ON CONFLICT (identifier) DO UPDATE SET + password_hash = EXCLUDED.password_hash, + status = 'active'; + +-- 3. Assign superadmin role (if user_roles table exists) +DO $$ +BEGIN + IF EXISTS (SELECT FROM pg_tables WHERE tablename = 'user_roles') THEN + INSERT INTO user_roles (user_id, role) + SELECT id, 'superadmin' FROM users WHERE identifier = 'superadmin' + ON CONFLICT (user_id, role) DO NOTHING; + END IF; +END $$; --- NOTE: user_roles table is created in migration 020, so we skip role assignment here. --- The seeder-api will handle proper user creation with all fields. diff --git a/backend/tests/verify_login_test.go b/backend/tests/verify_login_test.go index 20987c1..a20d6e0 100644 --- a/backend/tests/verify_login_test.go +++ b/backend/tests/verify_login_test.go @@ -58,3 +58,28 @@ func TestVerifyLogin(t *testing.T) { t.Logf("SUCCESS! Password verifies with pepper '%s'", pepper) } } + +func TestVerifyLoginNoPepper(t *testing.T) { + dbURL := "postgres://yuki:xl1zfmr6e9bb@db-60059.dc-sp-1.absamcloud.com:26868/gohorsejobs_dev?sslmode=require" + password := "Admin@2025!" + + db, err := sql.Open("postgres", dbURL) + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + defer db.Close() + + var hash string + err = db.QueryRow("SELECT password_hash FROM users WHERE identifier = 'superadmin'").Scan(&hash) + if err != nil { + t.Fatalf("Failed to find user: %v", err) + } + + // Try WITHOUT pepper + err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + if err == nil { + t.Log("✅ MATCH: Hash was created WITHOUT pepper") + } else { + t.Errorf("❌ No match without pepper either: %v", err) + } +} diff --git a/frontend/src/lib/__tests__/api.test.ts b/frontend/src/lib/__tests__/api.test.ts index e0e3184..456d384 100644 --- a/frontend/src/lib/__tests__/api.test.ts +++ b/frontend/src/lib/__tests__/api.test.ts @@ -1,19 +1,22 @@ let jobsApi: any let companiesApi: any let usersApi: any +let adminCompaniesApi: any // Mock environment variable const ORIGINAL_ENV = process.env beforeEach(() => { jest.resetModules() - process.env = { ...process.env, NEXT_PUBLIC_API_URL: 'http://test-api.com/api/v1' } + // API_BASE_URL should be just the domain - endpoints already include /api/v1 + process.env = { ...process.env, NEXT_PUBLIC_API_URL: 'http://test-api.com' } // Re-require modules to pick up new env vars const api = require('../api') jobsApi = api.jobsApi companiesApi = api.companiesApi usersApi = api.usersApi + adminCompaniesApi = api.adminCompaniesApi global.fetch = jest.fn() }) @@ -38,7 +41,7 @@ describe('API Client', () => { json: async () => mockJobs, }) - const response = await jobsApi.list({ page: 1, limit: 10, companyId: 5 }) + const response = await jobsApi.list({ page: 1, limit: 10, companyId: '5' }) expect(global.fetch).toHaveBeenCalledWith( 'http://test-api.com/api/v1/jobs?page=1&limit=10&companyId=5', @@ -55,10 +58,33 @@ describe('API Client', () => { ; (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 404, - text: async () => 'Not Found', + json: async () => ({ message: 'Not Found' }), }) - await expect(jobsApi.list()).rejects.toThrow('Not Found') + await expect(jobsApi.list({ page: 1, limit: 10 })).rejects.toThrow('Not Found') + }) + + it('should build URL with all filter parameters', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ data: [], pagination: {} }), + }) + + await jobsApi.list({ + page: 1, + limit: 20, + q: 'developer', + location: 'sp', + type: 'full-time', + workMode: 'remote' + }) + + const calledUrl = (global.fetch as jest.Mock).mock.calls[0][0] + expect(calledUrl).toContain('page=1') + expect(calledUrl).toContain('limit=20') + expect(calledUrl).toContain('q=developer') + expect(calledUrl).toContain('location=sp') + expect(calledUrl).toContain('workMode=remote') }) }) @@ -71,7 +97,7 @@ describe('API Client', () => { }) const newCompany = { name: 'Test Corp', slug: 'test-corp' } - await companiesApi.create(newCompany) + await adminCompaniesApi.create(newCompany) expect(global.fetch).toHaveBeenCalledWith( 'http://test-api.com/api/v1/companies', @@ -83,25 +109,59 @@ describe('API Client', () => { }) }) - describe('URL Construction', () => { - it('should handle double /api/v1 correctly', async () => { + describe('usersApi', () => { + it('should list users with pagination', async () => { ; (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ([]), + json: async () => ({ data: [], pagination: { total: 0 } }), }) - // We need to import the raw apiRequest function to test this properly, - // but since it's not exported, we simulate via an exported function - await usersApi.list() - - // The apiRequest logic handles URL cleaning. - // Expected: base http://test-api.com/api/v1 + endpoint /api/v1/users -> http://test-api.com/api/v1/users - // NOT http://test-api.com/api/v1/api/v1/users + await usersApi.list({ page: 1, limit: 10 }) expect(global.fetch).toHaveBeenCalledWith( - 'http://test-api.com/api/v1/users', + 'http://test-api.com/api/v1/users?page=1&limit=10', expect.any(Object) ) }) }) + + describe('URL Construction', () => { + it('should not have double /api/v1 in URL', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ data: [], pagination: {} }), + }) + + await usersApi.list({ page: 1, limit: 10 }) + + const calledUrl = (global.fetch as jest.Mock).mock.calls[0][0] + // Should NOT contain /api/v1/api/v1 + expect(calledUrl).not.toContain('/api/v1/api/v1') + // Should contain exactly one /api/v1 + expect(calledUrl).toBe('http://test-api.com/api/v1/users?page=1&limit=10') + }) + }) + + describe('Error Handling', () => { + it('should throw error with message from API', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + json: async () => ({ message: 'Unauthorized' }), + }) + + await expect(usersApi.list({ page: 1, limit: 10 })).rejects.toThrow('Unauthorized') + }) + + it('should throw generic error when no message', async () => { + ; (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 500, + json: async () => ({}), + }) + + await expect(usersApi.list({ page: 1, limit: 10 })).rejects.toThrow('Request failed with status 500') + }) + }) }) + diff --git a/seeder-api/.gitignore b/seeder-api/.gitignore index 23a4e98..15729c4 100644 --- a/seeder-api/.gitignore +++ b/seeder-api/.gitignore @@ -18,3 +18,12 @@ Thumbs.db # Git .git/ + +# SQL Data Files (large - download from GeoDB Cities github) +# https://github.com/dr5hn/countries-states-cities-database +sql/cities.sql.gz +sql/world.sql.gz +sql/states.sql +sql/countries.sql +sql/subregions.sql +sql/regions.sql diff --git a/seeder-api/package.json b/seeder-api/package.json index 6e7e175..c8624f2 100644 --- a/seeder-api/package.json +++ b/seeder-api/package.json @@ -6,6 +6,8 @@ "type": "module", "scripts": { "seed": "node src/index.js", + "seed:lite": "node src/index.js --lite", + "seed:fast": "node src/index.js --skip-locations", "seed:reset": "node src/index.js --reset", "migrate": "node src/migrate.js", "seed:users": "node src/seeders/users.js", diff --git a/seeder-api/src/index.js b/seeder-api/src/index.js index 9218ab4..91d1adc 100644 --- a/seeder-api/src/index.js +++ b/seeder-api/src/index.js @@ -1,5 +1,5 @@ import { pool, testConnection, closePool } from './db.js'; -import { seedLocationData } from './seeders/location-loader.js'; +import { seedLocationData, seedLocationDataLite } from './seeders/location-loader.js'; import { seedUsers } from './seeders/users.js'; import { seedCompanies } from './seeders/companies.js'; import { seedJobs } from './seeders/jobs.js'; @@ -108,14 +108,93 @@ async function seedDatabase() { } } +// Lite version (skips cities for faster seeding) +async function seedDatabaseLite() { + console.log('🌱 Starting database seeding (LITE - no cities)...\n'); + console.log('🌶️ PASSWORD_PEPPER:', process.env.PASSWORD_PEPPER ? `"${process.env.PASSWORD_PEPPER}"` : '(not set)'); + + try { + const connected = await testConnection(); + if (!connected) { + throw new Error('Could not connect to database'); + } + + console.log(''); + + // Seed in order (respecting foreign key dependencies) + await seedLocationDataLite(); // ⚡ Fast mode - no cities + await seedCompanies(); + await seedUsers(); + await seedJobs(); + await seedAcmeCorp(); + await seedWileECoyote(); + await seedFictionalCompanies(); + await seedEpicCompanies(); + await seedApplications(); + await seedNotifications(); + + console.log('\n✅ Database seeding (LITE) completed successfully!'); + console.log(' ⚡ Cities skipped for faster seeding'); + + } catch (error) { + console.error('\n❌ Seeding failed:', error.message); + console.error(error); + } finally { + await closePool(); + } +} + +// Ultra-fast version (skips ALL location data) +async function seedDatabaseNoLocations() { + console.log('🌱 Starting database seeding (NO LOCATIONS)...\n'); + console.log('🌶️ PASSWORD_PEPPER:', process.env.PASSWORD_PEPPER ? `"${process.env.PASSWORD_PEPPER}"` : '(not set)'); + console.log('⏭️ Skipping ALL location data (continents, countries, states, cities)\n'); + + try { + const connected = await testConnection(); + if (!connected) { + throw new Error('Could not connect to database'); + } + + console.log(''); + + // Skip location data entirely - just seed business data + await seedCompanies(); + await seedUsers(); + await seedJobs(); + await seedAcmeCorp(); + await seedWileECoyote(); + await seedFictionalCompanies(); + await seedEpicCompanies(); + await seedApplications(); + await seedNotifications(); + + console.log('\n✅ Database seeding (NO LOCATIONS) completed successfully!'); + console.log(' ⏭️ All location data skipped (continents, countries, states, cities)'); + + } catch (error) { + console.error('\n❌ Seeding failed:', error.message); + console.error(error); + } finally { + await closePool(); + } +} + // Main execution const shouldReset = process.argv.includes('--reset'); +const shouldLite = process.argv.includes('--lite'); +const shouldSkipLocations = process.argv.includes('--skip-locations'); (async () => { if (shouldReset) { await resetDatabase(); console.log('✅ Database reset complete. Run migrations before seeding.'); + } else if (shouldSkipLocations) { + await seedDatabaseNoLocations(); + } else if (shouldLite) { + await seedDatabaseLite(); } else { await seedDatabase(); } })(); + diff --git a/seeder-api/src/seeders/location-loader.js b/seeder-api/src/seeders/location-loader.js index e38b16e..cc9c316 100644 --- a/seeder-api/src/seeders/location-loader.js +++ b/seeder-api/src/seeders/location-loader.js @@ -463,6 +463,56 @@ export async function seedLocationData() { } } +/** + * Seed location data WITHOUT cities (fast mode for development) + * Skips the ~153k cities import for faster database reset + */ +export async function seedLocationDataLite() { + console.log('🌍 Seeding location data (LITE - no cities)...'); + console.log(' Source: GeoDB Cities (https://github.com/dr5hn/countries-states-cities-database)'); + console.log(' ⚡ Skipping cities for faster seeding\n'); + + try { + // 1. Continents (from regions.sql - 6 records) + console.log('1️⃣ Seeding Continents...'); + await executeSqlFile('regions.sql', 'continents'); + + // 2. Subregions (22 records) + console.log('2️⃣ Seeding Subregions...'); + await executeSqlFile('subregions.sql', 'subregions'); + + // 3. Countries (~250 records) + console.log('3️⃣ Seeding Countries...'); + await executeSqlFile('countries.sql', 'countries'); + + // 4. States (~5400 records) + console.log('4️⃣ Seeding States...'); + await executeSqlFile('states.sql', 'states'); + + // 5. Skip cities + console.log('5️⃣ ⏭️ Skipping Cities (use full seed for cities)\n'); + + console.log(' ✅ Location data LITE seeding complete!'); + + // Print counts (cities will be 0) + const counts = await pool.query(` + SELECT + (SELECT COUNT(*) FROM continents) as continents, + (SELECT COUNT(*) FROM subregions) as subregions, + (SELECT COUNT(*) FROM countries) as countries, + (SELECT COUNT(*) FROM states) as states, + (SELECT COUNT(*) FROM cities) as cities + `); + + const c = counts.rows[0]; + console.log(` 📊 Totals: ${c.continents} continents, ${c.subregions} subregions, ${c.countries} countries, ${c.states} states, ${c.cities} cities`); + + } catch (error) { + console.error('❌ Location seeding failed:', error.message); + throw error; + } +} + // For direct execution if (process.argv[1] === fileURLToPath(import.meta.url)) { import('../db.js').then(async ({ testConnection, closePool }) => { diff --git a/seeder-api/src/seeders/users.js b/seeder-api/src/seeders/users.js index 33f1487..fdc7722 100644 --- a/seeder-api/src/seeders/users.js +++ b/seeder-api/src/seeders/users.js @@ -7,6 +7,7 @@ const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || ''; export async function seedUsers() { console.log('👤 Seeding users (Unified Architecture)...'); + console.log(' ℹ️ SuperAdmin is created via backend migration (010_seed_super_admin.sql)'); try { // Fetch companies to map users (now using companies table, not core_companies) @@ -18,28 +19,10 @@ export async function seedUsers() { const systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'"); const systemTenantId = systemResult.rows[0]?.id || null; - // 1. Create SuperAdmin (Requested: superadmin / Admin@2025!) - const superAdminPassword = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10); + // NOTE: SuperAdmin is now created via migration 010_seed_super_admin.sql + // No longer created here to avoid PASSWORD_PEPPER mismatch issues - const result = await pool.query(` - INSERT INTO users (identifier, password_hash, role, full_name, email, name, tenant_id, status) - VALUES ($1, $2, 'superadmin', $3, $4, $5, $6, 'ACTIVE') - ON CONFLICT (identifier) DO UPDATE SET password_hash = EXCLUDED.password_hash - RETURNING id - `, ['superadmin', superAdminPassword, 'System Administrator', 'admin@gohorsejobs.com', 'System Administrator', systemTenantId]); - - const superAdminId = result.rows[0].id; - - // Role in user_roles table - await pool.query(` - INSERT INTO user_roles (user_id, role) - VALUES ($1, 'superadmin') - ON CONFLICT (user_id, role) DO NOTHING - `, [superAdminId]); - - console.log(' ✓ SuperAdmin created (superadmin)'); - - // 2. Create Company Admins + // 1. Create Company Admins const admins = [ { identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin'] }, { identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['admin'] }, diff --git a/start.sh b/start.sh index 2aabd07..96a1257 100755 --- a/start.sh +++ b/start.sh @@ -40,8 +40,12 @@ echo -e "" echo -e " ${CYAN}── Testing ──${NC}" echo -e " ${YELLOW}7)${NC} 🧪 Run Tests (Backend E2E)" echo -e " ${YELLOW}0)${NC} ❌ Exit" +echo -e "" +echo -e " ${CYAN}── Fast Options ──${NC}" +echo -e " ${YELLOW}8)${NC} ⚡ Seed Reset LITE (skip 153k cities)" +echo -e " ${YELLOW}9)${NC} 🔬 Run All Tests (Backend + Frontend)" echo "" -read -p "Enter option [0-7]: " choice +read -p "Enter option [0-9]: " choice case $choice in 1) @@ -206,6 +210,61 @@ case $choice in cd backend && go test -tags=e2e -v ./tests/e2e/... 2>&1 echo -e "\n${GREEN}✅ Tests completed!${NC}" ;; + + 8) + echo -e "\n${GREEN}⚡ Fast Reset - Seed LITE (no cities)...${NC}\n" + cd seeder-api + [ ! -d "node_modules" ] && npm install + + echo -e "${YELLOW}⚠️ This will DROP all tables and recreate (WITHOUT 153k cities)${NC}" + read -p "Are you sure? [y/N]: " confirm + + if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then + echo -e "\n${BLUE}🔹 Step 1/3: Dropping all tables...${NC}" + npm run seed:reset + + echo -e "\n${BLUE}🔹 Step 2/3: Running migrations...${NC}" + npm run migrate + + echo -e "\n${BLUE}🔹 Step 3/3: Seeding data (LITE - no cities)...${NC}" + npm run seed:lite + + echo -e "\n${GREEN}✅ Database reset (LITE) completed! Cities skipped for speed.${NC}" + else + echo -e "${YELLOW}Cancelled.${NC}" + fi + ;; + + 9) + echo -e "\n${GREEN}🔬 Running All Tests...${NC}\n" + + echo -e "${BLUE}🔹 Backend Unit Tests...${NC}" + cd backend && go test -v ./... -count=1 2>&1 | tail -20 + BACKEND_RESULT=$? + cd .. + + echo -e "\n${BLUE}🔹 Backend E2E Tests...${NC}" + cd backend && go test -tags=e2e -v ./tests/e2e/... 2>&1 + E2E_RESULT=$? + cd .. + + if [ -d "frontend/node_modules" ]; then + echo -e "\n${BLUE}🔹 Frontend Tests...${NC}" + cd frontend && npm test -- --passWithNoTests 2>&1 + FRONTEND_RESULT=$? + cd .. + else + echo -e "${YELLOW}⚠️ Frontend node_modules not found, skipping frontend tests${NC}" + FRONTEND_RESULT=0 + fi + + echo -e "\n${GREEN}═══════════════════════════════════════${NC}" + if [ $BACKEND_RESULT -eq 0 ] && [ $E2E_RESULT -eq 0 ] && [ $FRONTEND_RESULT -eq 0 ]; then + echo -e "${GREEN}✅ All tests passed!${NC}" + else + echo -e "${RED}❌ Some tests failed${NC}" + fi + ;; 0) echo -e "${YELLOW}Bye! 👋${NC}"