Proyecto App Linux
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Stalker Mock Server Architecture
|
||||
|
||||
This document describes the design decisions, data flow, and extension points of the `stalker-mock-server` development tool.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Stalker Portal Architecture](./stalker-portal.md)
|
||||
- [Stalker EPG Architecture](./stalker-epg.md)
|
||||
|
||||
## Purpose
|
||||
|
||||
The mock server enables:
|
||||
|
||||
1. **Local development** without access to a real Stalker portal
|
||||
2. **Playwright E2E testing** with predictable, deterministic data
|
||||
3. **Scenario-based testing** via predefined MAC addresses that map to specific data shapes
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Seeded Determinism (Not Per-Request Random)
|
||||
|
||||
Per-request random data would break navigation: if category IDs change between calls, content fetched under a category ID won't match the category list. Instead:
|
||||
|
||||
- Data is generated **once per MAC address** on first request, then cached in memory.
|
||||
- `@faker-js/faker` is seeded with a numeric value derived from the MAC address before generation.
|
||||
- Same MAC → identical data on every server restart.
|
||||
- Restart the server to reshuffle all data.
|
||||
|
||||
### MAC Address as Identity
|
||||
|
||||
Stalker portals use MAC address as the primary credential. The mock server follows the same model:
|
||||
|
||||
- Each unique MAC gets its own isolated dataset.
|
||||
- Predefined MACs map to specific `ScenarioConfig` shapes (see `src/app/scenarios.ts`).
|
||||
- Unknown MACs use the sum of their byte values as a seed, producing unique but deterministic data.
|
||||
|
||||
### In-Memory Only
|
||||
|
||||
No files or databases are written. All state (generated content + favorites) lives in process memory and resets on server restart. This is intentional — tests should not share state across runs.
|
||||
|
||||
## Data Generation Pipeline
|
||||
|
||||
```
|
||||
faker.seed(macToNumber(mac))
|
||||
│
|
||||
├── generateCategories('itv', N) → itvCategories[]
|
||||
│ └── generateChannels() → channels Map<categoryId, channel[]>
|
||||
│ └── generateEpg() → epg Map<channelId, program[]>
|
||||
│
|
||||
├── generateCategories('vod', N) → vodCategories[]
|
||||
│ └── generateVodItems() → vod Map<categoryId, item[]>
|
||||
│ ├── normal VOD items
|
||||
│ ├── is_series=1 items (fraction, Ministra flow)
|
||||
│ └── embedded series[] items (fraction)
|
||||
│
|
||||
└── generateCategories('series', N) → seriesCategories[]
|
||||
└── generateSeriesItems() → series Map<categoryId, item[]>
|
||||
└── generateSeasons() → seasons Map<seriesItemId, season[]>
|
||||
```
|
||||
|
||||
## Response Shapes
|
||||
|
||||
All responses follow the Stalker `portal.php` envelope:
|
||||
|
||||
```json
|
||||
{ "js": <action-specific payload> }
|
||||
```
|
||||
|
||||
### `get_categories`
|
||||
|
||||
```json
|
||||
{
|
||||
"js": [
|
||||
{ "id": "2001", "title": "Action", "alias": "action" },
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `get_ordered_list` (content)
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "20001",
|
||||
"name": "...",
|
||||
"cmd": "ffrt4://vod/20001/index.m3u8",
|
||||
"screenshot_uri": "https://picsum.photos/seed/vod-20001/300/200",
|
||||
"cover": "https://picsum.photos/seed/vod-cover-20001/300/450",
|
||||
"description": "...",
|
||||
"actors": "...",
|
||||
"director": "...",
|
||||
"year": "2019",
|
||||
"rating_imdb": "7.3",
|
||||
"category_id": "2001",
|
||||
"is_series": 0,
|
||||
"has_files": 1
|
||||
}
|
||||
],
|
||||
"total_items": 40,
|
||||
"max_page_items": 14,
|
||||
"cur_page": 1,
|
||||
"total_pages": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `get_ordered_list` (seasons — when `movie_id` is present)
|
||||
|
||||
```json
|
||||
{
|
||||
"js": [
|
||||
{
|
||||
"id": "30001-s1",
|
||||
"name": "Season 1",
|
||||
"cmd": "ffrt4://series/30001/season/1",
|
||||
"series": ["30001-s1-e1", "30001-s1-e2", ...],
|
||||
"screenshot_uri": "https://picsum.photos/seed/30001-s1/300/200",
|
||||
"director": "...",
|
||||
"actors": "...",
|
||||
"year": "2021",
|
||||
"rating_imdb": "8.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `create_link`
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"cmd": "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
|
||||
"streamer_id": "1",
|
||||
"load": "",
|
||||
"error": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The stream URL is selected from a pool of 4 real public HLS test streams. The choice is deterministic based on the `cmd` field's character sum, so the same item always returns the same stream.
|
||||
|
||||
### `get_short_epg`
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`get_short_epg` returns the current program and upcoming items from the
|
||||
generated schedule, limited by the requested `size`.
|
||||
|
||||
### `get_epg_info`
|
||||
|
||||
```json
|
||||
{
|
||||
"js": {
|
||||
"data": {
|
||||
"10000": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Channel Name: Program Title",
|
||||
"start": "2026-02-21T10:00:00.000Z",
|
||||
"stop": "2026-02-21T12:00:00.000Z",
|
||||
"start_timestamp": 1740128400,
|
||||
"stop_timestamp": 1740135600,
|
||||
"descr": "...",
|
||||
"category": "News"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`get_epg_info` returns bulk EPG keyed by channel id and filters the generated
|
||||
7-day schedule from the current UTC day start through `now + period`.
|
||||
|
||||
EPG programs are generated as 2-hour slots across 7 days for each channel,
|
||||
starting at the current UTC day boundary.
|
||||
|
||||
## Scenarios
|
||||
|
||||
Scenarios are defined in `src/app/scenarios.ts`. Each scenario is a `ScenarioConfig`:
|
||||
|
||||
```typescript
|
||||
interface ScenarioConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
seed: number;
|
||||
categoryCount: { itv: number; vod: number; series: number };
|
||||
itemsPerCategory: number;
|
||||
seasonsPerSeries: number;
|
||||
episodesPerSeason: number;
|
||||
isSeriesFraction: number; // 0–1: fraction of VOD with is_series=1
|
||||
embeddedSeriesFraction: number; // 0–1: fraction of VOD with embedded series[]
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Scenario
|
||||
|
||||
1. Add an entry to the `SCENARIOS` map in `src/app/scenarios.ts`.
|
||||
2. Use any unique MAC address as the key (lowercase, colon-separated).
|
||||
3. Document it in `README.md` and this file.
|
||||
|
||||
## Favorites
|
||||
|
||||
Favorites are stored in a `Map<mac, Set<itemId>>` in `src/app/data-store.ts`. They persist for the lifetime of the server process and are shared across all requests for the same MAC.
|
||||
|
||||
Call `POST /reset` to clear all favorites (and regenerated data) between test runs.
|
||||
|
||||
## Playwright Integration
|
||||
|
||||
`apps/web-e2e/playwright.config.ts` registers the mock server as a second `webServer` entry:
|
||||
|
||||
```typescript
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm nx run web:serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
url: 'http://localhost:3210/health',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
Playwright waits for both servers to be healthy before starting tests. If either is already running (e.g. in local dev), it reuses the existing instance.
|
||||
|
||||
### Test Isolation
|
||||
|
||||
Each stalker e2e test calls `POST http://localhost:3210/reset` in `beforeEach` to clear in-memory state. This ensures tests don't bleed favorites or other mutable state into each other.
|
||||
|
||||
The generated content (categories, items) is **not** cleared on reset — it's deterministic and doesn't need to be. Only in-memory favorites are cleared.
|
||||
|
||||
### Recommended Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const MOCK_URL = 'http://localhost:3210/portal.php';
|
||||
const MOCK_MAC = '00:1A:79:00:00:01'; // default scenario
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('http://localhost:3210/reset');
|
||||
});
|
||||
|
||||
test('browse VOD categories', async ({ page }) => {
|
||||
// Add portal via UI or programmatically via IndexedDB
|
||||
// Navigate to portal
|
||||
// Assert category list matches expected count (8 for default scenario)
|
||||
});
|
||||
```
|
||||
|
||||
## Extension Points
|
||||
|
||||
- **New content types**: Add a new generator function in `data-generator.ts` and a new handler in `handlers/`.
|
||||
- **New scenarios**: Add to `SCENARIOS` in `scenarios.ts`.
|
||||
- **Stateful session tokens**: `handshake.handler.ts` generates a token from the MAC — extend this to track token expiry for testing re-auth flows.
|
||||
- **Error simulation**: Add a special MAC or query param to trigger error responses (e.g. 401, 500) for testing error handling in the Stalker store.
|
||||
- **Slow responses**: Add a `MOCK_DELAY_MS` env var and apply it in middleware for testing loading states.
|
||||
Reference in New Issue
Block a user