feat: expand testing, add fast seeder options, hardcode superadmin

- start.sh: Add options 8 (Seed LITE - skip cities) and 9 (Run All Tests)
- seeder: Add seed:lite, seed:fast scripts and --skip-locations flag
- seeder: Remove superadmin creation (now via backend migration)
- backend: Update 010_seed_super_admin.sql with hardcoded hash (Admin@2025! + pepper)
- backend: Expand jwt_service_test.go with 5 new tests (+10% coverage)
- frontend: Fix api.test.ts URL duplication bug, add error handling tests
- seeder: Add SQL data files to .gitignore
This commit is contained in:
Tiago Yamamoto 2025-12-24 17:07:45 -03:00
parent dec9dc4897
commit d3c06f5564
11 changed files with 487 additions and 48 deletions

View file

@ -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))
}

View file

@ -207,3 +207,142 @@ func TestJWTService_NewJWTService(t *testing.T) {
fmt.Printf("[TEST LOG] Service created: %v\n", service) fmt.Printf("[TEST LOG] Service created: %v\n", service)
assert.NotNil(t, 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
}

View file

@ -1,7 +1,7 @@
-- Migration: Create Super Admin and System Company -- Migration: Create Super Admin and System Company
-- Description: Inserts the default System Company and Super Admin user. -- Description: Inserts the default System Company and Super Admin user.
-- Uses unified tables (companies, users, user_roles) -- 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) -- 1. Insert System Company (for SuperAdmin context)
INSERT INTO companies (name, slug, type, document, email, description, verified, active) INSERT INTO companies (name, slug, type, document, email, description, verified, active)
@ -16,17 +16,28 @@ VALUES (
true true
) ON CONFLICT (slug) DO NOTHING; ) ON CONFLICT (slug) DO NOTHING;
-- 2. Insert Super Admin User (using only columns from migration 001) -- 2. Insert Super Admin User
-- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER. -- Hash: bcrypt(Admin@2025! + gohorse-pepper) - HARDCODED for consistency
-- For development only. Use seeder-api for proper user creation with pepper. INSERT INTO users (identifier, password_hash, role, full_name, email, status, active)
INSERT INTO users (identifier, password_hash, role, full_name, active)
VALUES ( VALUES (
'superadmin', 'superadmin',
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder '$2a$10$/AodyEEQtKCjdeNThEUFee6QE/KvEBTzi1AnqQ78nwavkT1XFnw/6',
'superadmin', 'superadmin',
'Super Administrator', 'Super Administrator',
'admin@gohorsejobs.com',
'active',
true 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.

View file

@ -58,3 +58,28 @@ func TestVerifyLogin(t *testing.T) {
t.Logf("SUCCESS! Password verifies with pepper '%s'", pepper) 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)
}
}

View file

@ -1,19 +1,22 @@
let jobsApi: any let jobsApi: any
let companiesApi: any let companiesApi: any
let usersApi: any let usersApi: any
let adminCompaniesApi: any
// Mock environment variable // Mock environment variable
const ORIGINAL_ENV = process.env const ORIGINAL_ENV = process.env
beforeEach(() => { beforeEach(() => {
jest.resetModules() 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 // Re-require modules to pick up new env vars
const api = require('../api') const api = require('../api')
jobsApi = api.jobsApi jobsApi = api.jobsApi
companiesApi = api.companiesApi companiesApi = api.companiesApi
usersApi = api.usersApi usersApi = api.usersApi
adminCompaniesApi = api.adminCompaniesApi
global.fetch = jest.fn() global.fetch = jest.fn()
}) })
@ -38,7 +41,7 @@ describe('API Client', () => {
json: async () => mockJobs, 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( expect(global.fetch).toHaveBeenCalledWith(
'http://test-api.com/api/v1/jobs?page=1&limit=10&companyId=5', '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({ ; (global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false, ok: false,
status: 404, 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' } const newCompany = { name: 'Test Corp', slug: 'test-corp' }
await companiesApi.create(newCompany) await adminCompaniesApi.create(newCompany)
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
'http://test-api.com/api/v1/companies', 'http://test-api.com/api/v1/companies',
@ -83,25 +109,59 @@ describe('API Client', () => {
}) })
}) })
describe('URL Construction', () => { describe('usersApi', () => {
it('should handle double /api/v1 correctly', async () => { it('should list users with pagination', async () => {
; (global.fetch as jest.Mock).mockResolvedValueOnce({ ; (global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true, ok: true,
json: async () => ([]), json: async () => ({ data: [], pagination: { total: 0 } }),
}) })
// We need to import the raw apiRequest function to test this properly, await usersApi.list({ page: 1, limit: 10 })
// 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
expect(global.fetch).toHaveBeenCalledWith( 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) 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')
})
})
}) })

View file

@ -18,3 +18,12 @@ Thumbs.db
# Git # Git
.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

View file

@ -6,6 +6,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"seed": "node src/index.js", "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", "seed:reset": "node src/index.js --reset",
"migrate": "node src/migrate.js", "migrate": "node src/migrate.js",
"seed:users": "node src/seeders/users.js", "seed:users": "node src/seeders/users.js",

View file

@ -1,5 +1,5 @@
import { pool, testConnection, closePool } from './db.js'; 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 { seedUsers } from './seeders/users.js';
import { seedCompanies } from './seeders/companies.js'; import { seedCompanies } from './seeders/companies.js';
import { seedJobs } from './seeders/jobs.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 // Main execution
const shouldReset = process.argv.includes('--reset'); const shouldReset = process.argv.includes('--reset');
const shouldLite = process.argv.includes('--lite');
const shouldSkipLocations = process.argv.includes('--skip-locations');
(async () => { (async () => {
if (shouldReset) { if (shouldReset) {
await resetDatabase(); await resetDatabase();
console.log('✅ Database reset complete. Run migrations before seeding.'); console.log('✅ Database reset complete. Run migrations before seeding.');
} else if (shouldSkipLocations) {
await seedDatabaseNoLocations();
} else if (shouldLite) {
await seedDatabaseLite();
} else { } else {
await seedDatabase(); await seedDatabase();
} }
})(); })();

View file

@ -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 // For direct execution
if (process.argv[1] === fileURLToPath(import.meta.url)) { if (process.argv[1] === fileURLToPath(import.meta.url)) {
import('../db.js').then(async ({ testConnection, closePool }) => { import('../db.js').then(async ({ testConnection, closePool }) => {

View file

@ -7,6 +7,7 @@ const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || '';
export async function seedUsers() { export async function seedUsers() {
console.log('👤 Seeding users (Unified Architecture)...'); console.log('👤 Seeding users (Unified Architecture)...');
console.log(' SuperAdmin is created via backend migration (010_seed_super_admin.sql)');
try { try {
// Fetch companies to map users (now using companies table, not core_companies) // 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 systemResult = await pool.query("SELECT id FROM companies WHERE slug = 'gohorse-system'");
const systemTenantId = systemResult.rows[0]?.id || null; const systemTenantId = systemResult.rows[0]?.id || null;
// 1. Create SuperAdmin (Requested: superadmin / Admin@2025!) // NOTE: SuperAdmin is now created via migration 010_seed_super_admin.sql
const superAdminPassword = await bcrypt.hash('Admin@2025!' + PASSWORD_PEPPER, 10); // No longer created here to avoid PASSWORD_PEPPER mismatch issues
const result = await pool.query(` // 1. Create Company Admins
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
const admins = [ const admins = [
{ identifier: 'takeshi_yamamoto', fullName: 'Takeshi Yamamoto', company: 'TechCorp', email: 'takeshi@techcorp.com', pass: 'Takeshi@2025', roles: ['admin'] }, { 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'] }, { identifier: 'kenji', fullName: 'Kenji Tanaka', company: 'AppMakers', email: 'kenji@appmakers.mobile', pass: 'Takeshi@2025', roles: ['admin'] },

View file

@ -40,8 +40,12 @@ echo -e ""
echo -e " ${CYAN}── Testing ──${NC}" echo -e " ${CYAN}── Testing ──${NC}"
echo -e " ${YELLOW}7)${NC} 🧪 Run Tests (Backend E2E)" echo -e " ${YELLOW}7)${NC} 🧪 Run Tests (Backend E2E)"
echo -e " ${YELLOW}0)${NC} ❌ Exit" 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 "" echo ""
read -p "Enter option [0-7]: " choice read -p "Enter option [0-9]: " choice
case $choice in case $choice in
1) 1)
@ -207,6 +211,61 @@ case $choice in
echo -e "\n${GREEN}✅ Tests completed!${NC}" 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) 0)
echo -e "${YELLOW}Bye! 👋${NC}" echo -e "${YELLOW}Bye! 👋${NC}"
exit 0 exit 0