From b5ab3d7f132972d1320d2b0108d70d2e49d63e93 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 11 Jan 2026 02:28:02 +0000 Subject: [PATCH] Add test data generation feature with UI integration and demo script --- README.md | 25 +- TEST_DATA_GENERATOR_FEATURE.md | 248 +++++++++++++++++++ __pycache__/registration_gui.cpython-314.pyc | Bin 39648 -> 46114 bytes demo_test_data_ui.py | 180 ++++++++++++++ registration_gui.py | 158 +++++++++++- test_dummy_data.py | 154 ++++++++++++ 6 files changed, 753 insertions(+), 12 deletions(-) create mode 100644 TEST_DATA_GENERATOR_FEATURE.md create mode 100644 demo_test_data_ui.py create mode 100644 test_dummy_data.py diff --git a/README.md b/README.md index 283f6892..ca2f9d92 100644 --- a/README.md +++ b/README.md @@ -206,15 +206,24 @@ The Email Parser tab provides bi-directional functionality: [End Registration] ``` -#### Validation -- Click "Validate Key" on any tab to verify registration key correctness -- Uses exact string comparison matching the Java implementation -- Prompts for product name and secret key if needed -- Indicates valid/invalid keys with color-coded feedback +#### Test Data Generator +- Click **"Load Test Data"** button on any tab (blue/purple button) +- Automatically populates fields with realistic dummy data +- Generates **valid registration keys** that pass validation +- Includes first/last names, emails, products, secrets, and serials +- EmailParser tab: generates complete set including valid key +- Saves time during testing and development + +**Available Test Data**: +- 32 first names (James, Mary, Robert, Jennifer, etc.) +- 32 last names (Smith, Johnson, Williams, etc.) +- 10 email domains (gmail.com, company.com, etc.) +- 3 products: MegaLogViewer, TunerStudio, DataLogger +- 4 secrets: secret123, testkey, demo2024, abc123xyz +- 6 serial numbers: SN001, ABC123, XYZ789, etc. + +**Validation Guarantee**: Keys generated with test data are mathematically valid and will pass validation using the correct product/secret combination. -### Running the GUI -```bash -python3 registration_gui.py ``` ### Requirements diff --git a/TEST_DATA_GENERATOR_FEATURE.md b/TEST_DATA_GENERATOR_FEATURE.md new file mode 100644 index 00000000..b3579f34 --- /dev/null +++ b/TEST_DATA_GENERATOR_FEATURE.md @@ -0,0 +1,248 @@ +# Test Data Generator Feature + +## Summary + +Added automatic test data generation with "Load Test Data" buttons on all tabs. The system generates realistic dummy credentials and **valid registration keys** that pass validation. + +## Changes Made + +### 1. DummyDataGenerator Class + +**Location**: `registration_gui.py`, lines ~18-84 + +**Purpose**: Generate realistic test data for quick testing + +**Data Sets**: +- **First Names**: 32 common names (James, Mary, Robert, Jennifer, etc.) +- **Last Names**: 32 common surnames (Smith, Johnson, Williams, etc.) +- **Email Domains**: 10 realistic domains (gmail.com, company.com, etc.) +- **Products**: MegaLogViewer, TunerStudio, DataLogger +- **Secrets**: secret123, testkey, demo2024, abc123xyz +- **Serials**: SN001, ABC123, XYZ789, DEV001, TEST99 + +**Generation Logic**: +```python +data = DummyDataGenerator.generate() +# Returns: { +# 'first_name': 'John', +# 'last_name': 'Smith', +# 'email': 'john.smith@gmail.com', +# 'product': 'MegaLogViewer', +# 'secret': 'secret123', +# 'serial': 'SN001' +# } +``` + +### 2. Load Test Data Buttons + +#### KeyGeneratorTab (All Generation Tabs) +- **Button**: Blue "Load Test Data" button +- **Position**: Left of "Generate Registration Key" button +- **Behavior**: + 1. Generates random dummy data + 2. Populates all input fields + 3. Maps data correctly (including month='01', year='2015') + 4. Shows popup with generated data details + 5. Prompts user to click "Generate Registration Key" + +#### EmailParserTab +- **Button**: Purple "Load Test Data" button +- **Position**: Left of "Parse Email" button +- **Behavior**: + 1. Generates random dummy data + 2. **Automatically generates a VALID key** using 5-param algorithm + 3. Populates all output fields (name, email, serial, key) + 4. Shows popup with data AND validation instructions + 5. Includes product/secret in message for validation testing + +### 3. Validation Guarantees + +**Critical Feature**: Generated keys are mathematically valid! + +The EmailParserTab's `load_test_data()` method: +```python +# Generate VALID key using actual algorithm +valid_key = RegistrationKeyGenerator.generate_key_5param( + data['first_name'], + data['last_name'], + data['product'], + data['secret'], + data['email'] +) +``` + +This ensures: +- ✓ Keys pass exact string comparison (Java-style validation) +- ✓ Keys work with MD5 hash verification +- ✓ Keys use correct character set encoding +- ✓ Round-trip testing works (generate → parse → validate) + +### 4. Java Validation Analysis + +**Java Code Reviewed**: +- `bH/C0997e.java`: Key generation algorithms (4 variants) +- `com/efiAnalytics/ui/dS.java`: Registration dialog validation + +**Validation Requirements Found**: +1. **Exact Match**: Keys compared with `equals()` (line 297) +2. **No Tolerance**: No fuzzy matching, must be exact +3. **Case Sensitive**: Uses exact case from MD5 hash +4. **Character Set**: Must use correct base-32 character sets + - Basic (4-param): `"123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"` + - Enhanced (5/7/8-param): `"23456789ABCDEFGHJKLMNPQRSTUVWXYZ"` (no 1, I, O) + +**Python Implementation Compliance**: +- ✓ Uses identical character sets +- ✓ Implements same MD5 hashing (multiple rounds) +- ✓ Matches byte manipulation logic +- ✓ Produces identical output for same inputs + +### 5. Testing + +Created `test_dummy_data.py` with three test suites: + +#### Test 1: Data Generation +- Generates 5 random test sets +- Creates valid keys for each +- Displays all fields including keys + +#### Test 2: Validation +- Tests all 4 algorithms +- Verifies keys are deterministic (same input → same output) +- Confirms consistency + +#### Test 3: Email Format +- Generates complete registration email +- Shows proper format with markers +- Demonstrates round-trip capability + +**All tests passing ✓** + +## Usage Examples + +### Scenario 1: Quick Test on Any Tab + +1. Open GUI: `python3 registration_gui.py` +2. Go to any tab (e.g., "5-Parameter Standard") +3. Click blue **"Load Test Data"** button +4. See popup: "Test Data Loaded" with details +5. Click **"Generate Registration Key"** +6. Click **"Validate Key"** to verify it works +7. Result: ✓ Valid registration confirmed + +### Scenario 2: Email Parser with Pre-Generated Key + +1. Open GUI → **Email Parser** tab +2. Click purple **"Load Test Data"** button +3. See popup with product and secret (e.g., "Product: MegaLogViewer, Secret: secret123") +4. All fields now populated with valid data + key +5. Click **"Generate Email Format"** → formatted email created +6. Click **"Validate Key"** +7. Enter product="MegaLogViewer", secret="secret123" (from popup) +8. Result: ✓ Valid registration confirmed + +### Scenario 3: Round-Trip Testing + +1. **Generate Tab**: Load test data → Generate key → Copy +2. **Email Tab**: Load test data → Paste key → Generate email format +3. **Validation**: Parse email → Validate with correct product/secret +4. Result: Full workflow validated + +## Technical Details + +### Random Selection +```python +random.choice(DummyDataGenerator.FIRST_NAMES) # Picks one randomly +random.choice(DummyDataGenerator.PRODUCTS) # Ensures variety +``` + +### Email Construction +```python +email = f"{first_name.lower()}.{last_name.lower()}@{domain}" +# Example: "john.smith@gmail.com" +``` + +### Key Generation +```python +# EmailParserTab generates valid keys automatically +key = RegistrationKeyGenerator.generate_key_5param( + first_name, last_name, product, secret, email +) +# This key WILL pass validation with matching inputs +``` + +### Field Mapping +```python +field_mapping = { + 'first_name': data['first_name'], + 'last_name': data['last_name'], + 'email': data['email'], + 'product_name': data['product'], + 'secret': data['secret'], + 'serial': data['serial'], + 'month': '01', # Fixed for 7/8-param algorithms + 'year': '2015' # Fixed for 7/8-param algorithms +} +``` + +## Benefits + +1. **Speed**: No manual data entry for testing +2. **Accuracy**: Generated keys are guaranteed valid +3. **Variety**: Random selection provides different test cases +4. **Realism**: Names, emails look authentic +5. **Consistency**: Fixed secrets/serials for reproducibility +6. **Education**: Popup messages explain next steps +7. **Validation**: Can immediately test validation workflow + +## Files Modified + +- `registration_gui.py`: + - Added `DummyDataGenerator` class (lines ~18-84) + - Added `load_test_data()` method to `KeyGeneratorTab` (lines ~379-413) + - Added "Load Test Data" button to `KeyGeneratorTab` (lines ~277-294) + - Added `load_test_data()` method to `EmailParserTab` (lines ~872-907) + - Added "Load Test Data" button to `EmailParserTab` (lines ~596-612) + - Added `import random` (line 9) + +- Created `test_dummy_data.py`: Comprehensive test suite demonstrating functionality + +## Security Note + +**Test Data Only**: This is for testing/development. The fixed secrets (secret123, testkey, etc.) should never be used in production. Real implementations should use: +- Secure random secret generation +- Per-customer unique secrets +- Proper key management systems + +## Java Compatibility Verification + +Checked against Java source code: + +**✓ Character Sets Match**: +```java +// Java (C0997e.java line 64) +"123456789ABCDEFGHIJKLMNPQRSTUVWXYZ" // Basic +"23456789ABCDEFGHJKLMNPQRSTUVWXYZ" // Enhanced + +// Python (registration_gui.py lines 87-88) +CHARSET_BASIC = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ" +CHARSET_ENHANCED = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" +``` + +**✓ MD5 Hashing Matches**: +```java +// Java: messageDigest.update(bytes2); messageDigest.update(bytes2); +// Python: Uses identical double-update pattern +``` + +**✓ Validation Logic Matches**: +```java +// Java (dS.java line 297) +if (str2 != null && !str2.isEmpty() && str2.equals(strTrim4)) { + // Valid registration +} + +// Python: Uses string comparison: expected_key == reg_key +``` + +**Result**: Generated keys will be accepted by actual Java application. diff --git a/__pycache__/registration_gui.cpython-314.pyc b/__pycache__/registration_gui.cpython-314.pyc index 86f9c8c7c2e80607d46acea9a806152646198b10..811f62b98c2f61a650f1dea4af0f932609059766 100644 GIT binary patch delta 9979 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 delta 4777 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