feat(dm-dashboard): Wave 2 — Drizzle migration, DmSidebar fix, CORS config
@@ -394,7 +394,7 @@
|
|||||||
"plan_name": "dm-dashboard-ai",
|
"plan_name": "dm-dashboard-ai",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"started_at": "2026-05-15T17:58:09.859Z",
|
"started_at": "2026-05-15T17:58:09.859Z",
|
||||||
"updated_at": "2026-05-15T18:24:29.818Z",
|
"updated_at": "2026-05-15T18:36:00.096Z",
|
||||||
"session_ids": [
|
"session_ids": [
|
||||||
"ses_1d3921469ffeQox08ZjNWWJq7u"
|
"ses_1d3921469ffeQox08ZjNWWJq7u"
|
||||||
],
|
],
|
||||||
@@ -407,12 +407,12 @@
|
|||||||
"task_key": "final-wave:f1",
|
"task_key": "final-wave:f1",
|
||||||
"task_label": "F1",
|
"task_label": "F1",
|
||||||
"task_title": "**Plan Compliance Audit** — `oracle`",
|
"task_title": "**Plan Compliance Audit** — `oracle`",
|
||||||
"session_id": "ses_1d333a383ffeOqEVZptN3EbGAY",
|
"session_id": "ses_1d31971f9ffedffglMh0qVc3Mi",
|
||||||
"agent": "Sisyphus-Junior",
|
"agent": "Sisyphus-Junior",
|
||||||
"category": "quick",
|
"category": "quick",
|
||||||
"updated_at": "2026-05-15T18:24:29.818Z",
|
|
||||||
"started_at": "2026-05-15T18:24:29.818Z",
|
"started_at": "2026-05-15T18:24:29.818Z",
|
||||||
"status": "running"
|
"status": "running",
|
||||||
|
"updated_at": "2026-05-15T18:36:00.097Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +420,7 @@
|
|||||||
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/dm-dashboard-ai.md",
|
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/dm-dashboard-ai.md",
|
||||||
"started_at": "2026-05-15T17:58:09.859Z",
|
"started_at": "2026-05-15T17:58:09.859Z",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"updated_at": "2026-05-15T18:24:29.818Z",
|
"updated_at": "2026-05-15T18:36:00.096Z",
|
||||||
"session_ids": [
|
"session_ids": [
|
||||||
"ses_1d3921469ffeQox08ZjNWWJq7u"
|
"ses_1d3921469ffeQox08ZjNWWJq7u"
|
||||||
],
|
],
|
||||||
@@ -433,12 +433,12 @@
|
|||||||
"task_key": "final-wave:f1",
|
"task_key": "final-wave:f1",
|
||||||
"task_label": "F1",
|
"task_label": "F1",
|
||||||
"task_title": "**Plan Compliance Audit** — `oracle`",
|
"task_title": "**Plan Compliance Audit** — `oracle`",
|
||||||
"session_id": "ses_1d333a383ffeOqEVZptN3EbGAY",
|
"session_id": "ses_1d31971f9ffedffglMh0qVc3Mi",
|
||||||
"agent": "Sisyphus-Junior",
|
"agent": "Sisyphus-Junior",
|
||||||
"category": "quick",
|
"category": "quick",
|
||||||
"updated_at": "2026-05-15T18:24:29.818Z",
|
|
||||||
"started_at": "2026-05-15T18:24:29.818Z",
|
"started_at": "2026-05-15T18:24:29.818Z",
|
||||||
"status": "running"
|
"status": "running",
|
||||||
|
"updated_at": "2026-05-15T18:36:00.097Z"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent": "atlas"
|
"agent": "atlas"
|
||||||
|
|||||||
@@ -43,3 +43,90 @@
|
|||||||
|
|
||||||
- `npx tsc --noEmit`: 0 errors
|
- `npx tsc --noEmit`: 0 errors
|
||||||
- `npx vitest run src/lib/auth/`: 18/18 tests passed
|
- `npx vitest run src/lib/auth/`: 18/18 tests passed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# CORS Configuration for DM API Routes — Learnings
|
||||||
|
|
||||||
|
## Date: 2026-05-15
|
||||||
|
|
||||||
|
### Patterns Applied
|
||||||
|
|
||||||
|
1. **Reusable CORS utility (`src/lib/cors.ts`)**
|
||||||
|
- Created `getCorsHeaders()`, `createCorsResponse()`, `handleCorsPreflight()`, and `jsonResponse()` helpers.
|
||||||
|
- Allowed origins are validated by hostname using `URL` parsing: `randify.pro`, `dm.randify.pro`, `localhost:4321`.
|
||||||
|
- Disallowed origins receive an empty `Access-Control-Allow-Origin` header (no wildcard).
|
||||||
|
- `Vary: Origin` header is included to prevent CDN caching issues with CORS.
|
||||||
|
|
||||||
|
2. **DM API route pattern (`src/pages/api/dm/health.ts`)**
|
||||||
|
- New DM API routes should import helpers from `@/lib/cors`.
|
||||||
|
- Every route must export an `OPTIONS` handler that calls `handleCorsPreflight(origin)`.
|
||||||
|
- Every response should be created via `jsonResponse()` or `createCorsResponse()` to ensure CORS headers are present.
|
||||||
|
- `export const prerender = false` is required for API routes.
|
||||||
|
|
||||||
|
3. **Allowed origins**
|
||||||
|
- `https://randify.pro`
|
||||||
|
- `https://dm.randify.pro`
|
||||||
|
- `http://localhost:4321`
|
||||||
|
- Any protocol is accepted as long as the hostname matches.
|
||||||
|
|
||||||
|
4. **happy-dom `Request` limitation**
|
||||||
|
- `new Request(url, { headers: { origin: '...' } })` strips the `origin` header in happy-dom (vitest environment).
|
||||||
|
- Tests that call Astro API routes must construct a mock request object with a custom `headers.get()` method instead of relying on the global `Request` class for origin propagation.
|
||||||
|
- Example pattern in `tests/cors.test.ts` (`mockRequest` helper).
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- `npx vitest run tests/cors.test.ts`: 12/12 passed
|
||||||
|
- `npx vitest run` (full suite): 218/218 passed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drizzle Migration Generation — Learnings
|
||||||
|
|
||||||
|
## Date: 2026-05-15
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
Task: Generate and apply Drizzle migration for updated DB schema (Task 2 completed).
|
||||||
|
|
||||||
|
### What Was Done
|
||||||
|
|
||||||
|
1. **Generated migration via `drizzle-kit generate`**
|
||||||
|
- Command: `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/randify npx drizzle-kit generate`
|
||||||
|
- Generated file: `drizzle/0001_curved_black_panther.sql`
|
||||||
|
- Journal updated: `drizzle/meta/_journal.json`
|
||||||
|
|
||||||
|
2. **Reviewed generated SQL for correctness**
|
||||||
|
- New tables created: `npcs`, `generation_counters`, `translations`, `notes`, `initiative_sessions`
|
||||||
|
- Existing table altered: `users` — added `tier` (varchar(20), default 'free') and `boosty_verified_at` (timestamp)
|
||||||
|
- All FK constraints use `ON DELETE cascade` as specified in schema
|
||||||
|
- Indexes match schema definitions:
|
||||||
|
- `generation_counters_user_window_model_idx` (unique)
|
||||||
|
- `translations_slug_type_language_idx`
|
||||||
|
- CHECK constraint added: `users_tier_check` enforcing `('free', 'pro')`
|
||||||
|
- `translations.slug` correctly marked as `UNIQUE`
|
||||||
|
|
||||||
|
3. **Verified migration applies cleanly**
|
||||||
|
- Command: `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/randify npm run db:migrate`
|
||||||
|
- Result: Migrations completed successfully (exit 0)
|
||||||
|
- Verified tables in DB: 7 tables present (`users`, `sessions`, `npcs`, `generation_counters`, `translations`, `notes`, `initiative_sessions`)
|
||||||
|
|
||||||
|
4. **Created down migration for reversibility**
|
||||||
|
- File: `drizzle/0001_curved_black_panther.down.sql`
|
||||||
|
- Operations: drop new tables, remove added columns/constraints from `users`
|
||||||
|
- Tested down migration against local DB: executed cleanly
|
||||||
|
- Re-applied up migration after test to restore expected state
|
||||||
|
|
||||||
|
### Key Findings
|
||||||
|
|
||||||
|
- **`.env` DATABASE_URL mismatch**: `.env` points to `dmuser:dmpass@localhost:5432/dmdashboard`, but the docker-compose PostgreSQL uses `postgres:postgres@localhost:5432/randify`. For DB commands to work locally, `DATABASE_URL` must be overridden inline or `.env` must be updated.
|
||||||
|
- **Drizzle-kit does not auto-generate `.down.sql` files**. Reversibility must be handled manually. The down migration should drop tables in reverse dependency order and remove columns after dropping their constraints.
|
||||||
|
- **Testing down migrations**: After testing a down migration, reset `drizzle.__drizzle_migrations` and re-run the official `db:migrate` script rather than applying raw SQL, to keep Drizzle's internal tracking consistent.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- `npm run db:migrate` with correct `DATABASE_URL`: exit 0
|
||||||
|
- `\dt` in `randify` DB: 7 tables confirmed
|
||||||
|
- Down migration tested manually: all statements executed without error
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
ALTER TABLE "users" DROP CONSTRAINT "users_tier_check";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "tier";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" DROP COLUMN "boosty_verified_at";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "translations";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "npcs";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "notes";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "initiative_sessions";
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE "generation_counters";
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
CREATE TABLE "generation_counters" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"hour_window" timestamp NOT NULL,
|
||||||
|
"count" integer NOT NULL,
|
||||||
|
"model" varchar(50) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "initiative_sessions" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"participants" jsonb,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "notes" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"title" varchar(255) NOT NULL,
|
||||||
|
"content" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "npcs" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"race" varchar(100),
|
||||||
|
"role" varchar(100),
|
||||||
|
"level" integer,
|
||||||
|
"tone" varchar(100),
|
||||||
|
"content" jsonb,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "translations" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"slug" varchar(255) NOT NULL,
|
||||||
|
"type" varchar(100) NOT NULL,
|
||||||
|
"language" varchar(10) DEFAULT 'ru' NOT NULL,
|
||||||
|
"content" jsonb,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "translations_slug_unique" UNIQUE("slug")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD COLUMN "tier" varchar(20) DEFAULT 'free';--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD COLUMN "boosty_verified_at" timestamp;--> statement-breakpoint
|
||||||
|
ALTER TABLE "generation_counters" ADD CONSTRAINT "generation_counters_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "initiative_sessions" ADD CONSTRAINT "initiative_sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "notes" ADD CONSTRAINT "notes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "npcs" ADD CONSTRAINT "npcs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "generation_counters_user_window_model_idx" ON "generation_counters" USING btree ("user_id","hour_window","model");--> statement-breakpoint
|
||||||
|
CREATE INDEX "translations_slug_type_language_idx" ON "translations" USING btree ("slug","type","language");--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "users_tier_check" CHECK ("users"."tier" IN ('free', 'pro'));
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
{
|
||||||
|
"id": "5a462f3f-9903-44f9-bc9c-58038e24e590",
|
||||||
|
"prevId": "993d57f2-7253-4ab4-b631-ab26ff2b9356",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.generation_counters": {
|
||||||
|
"name": "generation_counters",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"hour_window": {
|
||||||
|
"name": "hour_window",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"name": "count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"generation_counters_user_window_model_idx": {
|
||||||
|
"name": "generation_counters_user_window_model_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "hour_window",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "model",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"generation_counters_user_id_users_id_fk": {
|
||||||
|
"name": "generation_counters_user_id_users_id_fk",
|
||||||
|
"tableFrom": "generation_counters",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.initiative_sessions": {
|
||||||
|
"name": "initiative_sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"participants": {
|
||||||
|
"name": "participants",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"initiative_sessions_user_id_users_id_fk": {
|
||||||
|
"name": "initiative_sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "initiative_sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"notes_user_id_users_id_fk": {
|
||||||
|
"name": "notes_user_id_users_id_fk",
|
||||||
|
"tableFrom": "notes",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.npcs": {
|
||||||
|
"name": "npcs",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"race": {
|
||||||
|
"name": "race",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tone": {
|
||||||
|
"name": "tone",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"npcs_user_id_users_id_fk": {
|
||||||
|
"name": "npcs_user_id_users_id_fk",
|
||||||
|
"tableFrom": "npcs",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.translations": {
|
||||||
|
"name": "translations",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"name": "language",
|
||||||
|
"type": "varchar(10)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'ru'"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "jsonb",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"translations_slug_type_language_idx": {
|
||||||
|
"name": "translations_slug_type_language_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "language",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"translations_slug_unique": {
|
||||||
|
"name": "translations_slug_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"vk_id": {
|
||||||
|
"name": "vk_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"yandex_id": {
|
||||||
|
"name": "yandex_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"tier": {
|
||||||
|
"name": "tier",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "'free'"
|
||||||
|
},
|
||||||
|
"boosty_verified_at": {
|
||||||
|
"name": "boosty_verified_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"users_tier_check": {
|
||||||
|
"name": "users_tier_check",
|
||||||
|
"value": "\"users\".\"tier\" IN ('free', 'pro')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1778781758609,
|
"when": 1778781758609,
|
||||||
"tag": "0000_short_warpath",
|
"tag": "0000_short_warpath",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778869902855,
|
||||||
|
"tag": "0001_curved_black_panther",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ export interface Props {
|
|||||||
user?: {
|
user?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
tier?: 'free' | 'pro';
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ const { user } = Astro.props;
|
|||||||
<aside class="w-full">
|
<aside class="w-full">
|
||||||
<!-- User info (if logged in) -->
|
<!-- User info (if logged in) -->
|
||||||
{user && (
|
{user && (
|
||||||
<div class="flex items-center gap-3 mb-6 px-3 py-3 rounded-xl bg-[var(--bg-card)] border border-[var(--border-gold-strong)]">
|
<div data-testid="sidebar-user-block" class="flex items-center gap-3 mb-6 px-3 py-3 rounded-xl bg-[var(--bg-card)] border border-[var(--border-gold-strong)]">
|
||||||
{user.avatar ? (
|
{user.avatar ? (
|
||||||
<img src={user.avatar} alt="" class="w-8 h-8 rounded-full border-2 border-[var(--accent)]" />
|
<img src={user.avatar} alt="" class="w-8 h-8 rounded-full border-2 border-[var(--accent)]" />
|
||||||
) : (
|
) : (
|
||||||
@@ -22,9 +23,20 @@ const { user } = Astro.props;
|
|||||||
{user.name?.charAt(0).toUpperCase() || "?"}
|
{user.name?.charAt(0).toUpperCase() || "?"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span class="text-sm font-medium text-[var(--text-primary)] truncate">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
{user.name || "Пользователь"}
|
<span class="text-sm font-medium text-[var(--text-primary)] truncate">
|
||||||
</span>
|
{user.name || "Пользователь"}
|
||||||
|
</span>
|
||||||
|
{user.tier === 'pro' ? (
|
||||||
|
<span data-testid="tier-badge" data-tier="pro" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide bg-[var(--accent)] text-white">
|
||||||
|
{T.badgePro}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span data-testid="tier-badge" data-tier="free" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide border border-[var(--text-muted)] text-[var(--text-muted)]">
|
||||||
|
{T.badgeFree}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface User {
|
|||||||
email: string | null;
|
email: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
tier: 'free' | 'pro';
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export const dmTranslations = {
|
|||||||
loginVk: "Войти через VK",
|
loginVk: "Войти через VK",
|
||||||
loginYandex: "Войти через Яндекс",
|
loginYandex: "Войти через Яндекс",
|
||||||
logout: "Выйти",
|
logout: "Выйти",
|
||||||
|
badgeFree: "FREE",
|
||||||
|
badgePro: "PRO",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type DmT = typeof dmTranslations;
|
export type DmT = typeof dmTranslations;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* CORS utility for DM API routes.
|
||||||
|
* Allowed origins: randify.pro, dm.randify.pro, localhost:4321
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ALLOWED_HOSTS = new Set([
|
||||||
|
"randify.pro",
|
||||||
|
"dm.randify.pro",
|
||||||
|
"localhost:4321",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isAllowedOrigin(origin: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(origin);
|
||||||
|
return ALLOWED_HOSTS.has(url.host);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCorsHeaders(origin: string | null): Record<string, string> {
|
||||||
|
const allowedOrigin = origin && isAllowedOrigin(origin) ? origin : "";
|
||||||
|
return {
|
||||||
|
"Access-Control-Allow-Origin": allowedOrigin,
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
"Access-Control-Allow-Credentials": "true",
|
||||||
|
"Vary": "Origin",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCorsResponse(
|
||||||
|
body: BodyInit | null,
|
||||||
|
status: number,
|
||||||
|
origin: string | null,
|
||||||
|
extraHeaders?: Record<string, string>
|
||||||
|
): Response {
|
||||||
|
const headers = new Headers({
|
||||||
|
...getCorsHeaders(origin),
|
||||||
|
...extraHeaders,
|
||||||
|
});
|
||||||
|
return new Response(body, { status, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCorsPreflight(origin: string | null): Response {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: getCorsHeaders(origin),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonResponse(data: unknown, status: number, origin: string | null): Response {
|
||||||
|
return createCorsResponse(
|
||||||
|
JSON.stringify(data),
|
||||||
|
status,
|
||||||
|
origin,
|
||||||
|
{ "Content-Type": "application/json" }
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,7 +38,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|||||||
|
|
||||||
const user = userResult[0];
|
const user = userResult[0];
|
||||||
if (user) {
|
if (user) {
|
||||||
context.locals.user = user;
|
context.locals.user = {
|
||||||
|
...user,
|
||||||
|
tier: (user.tier ?? 'free') as 'free' | 'pro',
|
||||||
|
};
|
||||||
console.log('[Middleware] User attached to locals:', { userId: user.id, name: user.name });
|
console.log('[Middleware] User attached to locals:', { userId: user.id, name: user.name });
|
||||||
} else {
|
} else {
|
||||||
console.log('[Middleware] No user found for session');
|
console.log('[Middleware] No user found for session');
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { jsonResponse, handleCorsPreflight } from "@/lib/cors";
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
|
const origin = request.headers.get("origin");
|
||||||
|
return jsonResponse({ status: "ok", dm: true }, 200, origin);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OPTIONS: APIRoute = async ({ request }) => {
|
||||||
|
const origin = request.headers.get("origin");
|
||||||
|
return handleCorsPreflight(origin);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ import { dmTranslations as T } from "@/i18n/dm-translations";
|
|||||||
<DmLayout>
|
<DmLayout>
|
||||||
<!-- Sidebar slot (desktop only) -->
|
<!-- Sidebar slot (desktop only) -->
|
||||||
<div slot="sidebar">
|
<div slot="sidebar">
|
||||||
<DmSidebar />
|
<DmSidebar user={Astro.locals.user} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main slot (dice + initiative) -->
|
<!-- Main slot (dice + initiative) -->
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "passed",
|
|
||||||
"failedTests": []
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/initiative.spec.ts >> Initiative Tracker >> sort by initiative descending
|
||||||
|
- Location: tests/dm/initiative.spec.ts:16:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browserContext.close: Test ended.
|
||||||
|
Browser logs:
|
||||||
|
|
||||||
|
<launching> /home/emil/.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-acO0ks --remote-debugging-pipe --no-startup-window
|
||||||
|
<launched> pid=1566366
|
||||||
|
[pid=1566366][err] [0515/214659.519582:WARNING:media/gpu/vaapi/vaapi_wrapper.cc:123] Should skip nVidia device named: nvidia-drm
|
||||||
|
[pid=1566366][err] [0515/214659.522782:WARNING:sandbox/policy/linux/sandbox_linux.cc:404] InitializeSandbox() called with multiple threads in process gpu-process.
|
||||||
|
[pid=1566366][err] [0515/214700.398597:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566366][err] [0515/214700.669321:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566366][err] [0515/214701.278769:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566366][err] [0515/214702.051176:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566366][err] [0515/214702.141812:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566366][err] [0515/214702.233595:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566366][err] [0515/214703.793566:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566366][err] [0515/214703.945205:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566366][err] [0515/214705.122983:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566366] <gracefully close start>
|
||||||
|
```
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/initiative.spec.ts >> Initiative Tracker >> next turn cycles active combatant
|
||||||
|
- Location: tests/dm/initiative.spec.ts:31:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.click: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- waiting for locator('[data-testid=\'init-next-btn\']')
|
||||||
|
- locator resolved to <button type="button" id="it-next-btn" data-testid="init-next-btn" data-astro-source-loc="51:94" data-astro-source-file="/home/emil/Desktop/Coding/AI/Randify.pro/src/components/dm/DmButton.astro" class="inline-flex items-center justify-center font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-primary)] transition-all duration-[var(--transition-base)] cursor-pointer disabled:opacity-50 disabled:cursor-…>Следующий ход</button>
|
||||||
|
- attempting click action
|
||||||
|
- waiting for element to be visible, enabled and stable
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# Test source
|
||||||
|
|
||||||
|
```ts
|
||||||
|
1 | import { test, expect } from "@playwright/test";
|
||||||
|
2 |
|
||||||
|
3 | test.describe("Initiative Tracker", () => {
|
||||||
|
4 | test.beforeEach(async ({ page }) => {
|
||||||
|
5 | await page.goto("/dm/#initiative");
|
||||||
|
6 | });
|
||||||
|
7 |
|
||||||
|
8 | test("add combatant with auto-roll", async ({ page }) => {
|
||||||
|
9 | await page.fill("[data-testid='init-name-input']", "Гоблин");
|
||||||
|
10 | await page.fill("[data-testid='init-mod-input']", "2");
|
||||||
|
11 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
12 | await expect(page.locator("[data-testid='init-combatant']")).toBeVisible();
|
||||||
|
13 | await expect(page.locator("[data-testid='init-combatant-name']")).toContainText("Гоблин");
|
||||||
|
14 | });
|
||||||
|
15 |
|
||||||
|
16 | test("sort by initiative descending", async ({ page }) => {
|
||||||
|
17 | await page.fill("[data-testid='init-name-input']", "А");
|
||||||
|
18 | await page.fill("[data-testid='init-mod-input']", "5");
|
||||||
|
19 | await page.fill("[data-testid='init-initiative-input']", "25");
|
||||||
|
20 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
21 |
|
||||||
|
22 | await page.fill("[data-testid='init-name-input']", "Б");
|
||||||
|
23 | await page.fill("[data-testid='init-mod-input']", "0");
|
||||||
|
24 | await page.fill("[data-testid='init-initiative-input']", "10");
|
||||||
|
25 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
26 |
|
||||||
|
27 | const scores = await page.locator("[data-testid='init-score']").allTextContents();
|
||||||
|
28 | expect(Number(scores[0])).toBeGreaterThan(Number(scores[1]));
|
||||||
|
29 | });
|
||||||
|
30 |
|
||||||
|
31 | test("next turn cycles active combatant", async ({ page }) => {
|
||||||
|
32 | await page.fill("[data-testid='init-name-input']", "Гоблин");
|
||||||
|
33 | await page.fill("[data-testid='init-mod-input']", "2");
|
||||||
|
34 | await page.fill("[data-testid='init-initiative-input']", "20");
|
||||||
|
35 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
36 |
|
||||||
|
37 | await page.fill("[data-testid='init-name-input']", "Орк");
|
||||||
|
38 | await page.fill("[data-testid='init-mod-input']", "0");
|
||||||
|
39 | await page.fill("[data-testid='init-initiative-input']", "15");
|
||||||
|
40 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
41 |
|
||||||
|
42 | const firstActive = await page.locator("[data-testid='init-combatant'].active").textContent();
|
||||||
|
> 43 | await page.click("[data-testid='init-next-btn']");
|
||||||
|
| ^ Error: page.click: Target page, context or browser has been closed
|
||||||
|
44 | const secondActive = await page.locator("[data-testid='init-combatant'].active").textContent();
|
||||||
|
45 | expect(firstActive).not.toBe(secondActive);
|
||||||
|
46 | });
|
||||||
|
47 |
|
||||||
|
48 | test("delete combatant", async ({ page }) => {
|
||||||
|
49 | await page.fill("[data-testid='init-name-input']", "Гоблин");
|
||||||
|
50 | await page.fill("[data-testid='init-mod-input']", "2");
|
||||||
|
51 | await page.fill("[data-testid='init-initiative-input']", "20");
|
||||||
|
52 | await page.click("[data-testid='init-add-btn']");
|
||||||
|
53 |
|
||||||
|
54 | await page.click("[data-testid='init-delete-btn']");
|
||||||
|
55 | await expect(page.locator("[data-testid='init-empty-state']")).toBeVisible();
|
||||||
|
56 | });
|
||||||
|
57 |
|
||||||
|
58 | test("mobile viewport renders correctly", async ({ page }) => {
|
||||||
|
59 | await page.setViewportSize({ width: 375, height: 812 });
|
||||||
|
60 | await expect(page.locator("[data-testid='init-section']")).toBeVisible();
|
||||||
|
61 | });
|
||||||
|
62 | });
|
||||||
|
63 |
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/notes.spec.ts >> Notes Panel >> auto-save indicator appears
|
||||||
|
- Location: tests/dm/notes.spec.ts:13:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.goto: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- navigating to "http://localhost:4321/dm/#notes", waiting until "load"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browserContext.close: Test ended.
|
||||||
|
Browser logs:
|
||||||
|
|
||||||
|
<launching> /home/emil/.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-q9tkzK --remote-debugging-pipe --no-startup-window
|
||||||
|
<launched> pid=1566357
|
||||||
|
[pid=1566357][err] [0515/214659.521694:WARNING:media/gpu/vaapi/vaapi_wrapper.cc:123] Should skip nVidia device named: nvidia-drm
|
||||||
|
[pid=1566357][err] [0515/214659.524194:WARNING:sandbox/policy/linux/sandbox_linux.cc:404] InitializeSandbox() called with multiple threads in process gpu-process.
|
||||||
|
[pid=1566357][err] [0515/214700.560515:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566357][err] [0515/214700.656507:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566357][err] [0515/214701.495744:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566357][err] [0515/214702.900494:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566357][err] [0515/214703.014550:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566357][err] [0515/214703.021710:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566357][err] [0515/214704.867351:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566357][err] [0515/214705.018298:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566357][err] [0515/214705.698751:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566357] <gracefully close start>
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/notes.spec.ts >> Notes Panel >> character counter updates
|
||||||
|
- Location: tests/dm/notes.spec.ts:25:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.goto: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- navigating to "http://localhost:4321/dm/#notes", waiting until "load"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browserContext.close: Target page, context or browser has been closed
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/notes.spec.ts >> Notes Panel >> clear button empties notes
|
||||||
|
- Location: tests/dm/notes.spec.ts:19:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.goto: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- navigating to "http://localhost:4321/dm/#notes", waiting until "load"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browserContext.close: Target page, context or browser has been closed
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/notes.spec.ts >> Notes Panel >> mobile viewport renders correctly
|
||||||
|
- Location: tests/dm/notes.spec.ts:30:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.goto: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- navigating to "http://localhost:4321/dm/#notes", waiting until "load"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browserContext.close: Target page, context or browser has been closed
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/notes.spec.ts >> Notes Panel >> typing in textarea works
|
||||||
|
- Location: tests/dm/notes.spec.ts:8:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: page.goto: Target page, context or browser has been closed
|
||||||
|
Call log:
|
||||||
|
- navigating to "http://localhost:4321/dm/#notes", waiting until "load"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# Test source
|
||||||
|
|
||||||
|
```ts
|
||||||
|
1 | import { test, expect } from "@playwright/test";
|
||||||
|
2 |
|
||||||
|
3 | test.describe("Notes Panel", () => {
|
||||||
|
4 | test.beforeEach(async ({ page }) => {
|
||||||
|
> 5 | await page.goto("/dm/#notes");
|
||||||
|
| ^ Error: page.goto: Target page, context or browser has been closed
|
||||||
|
6 | });
|
||||||
|
7 |
|
||||||
|
8 | test("typing in textarea works", async ({ page }) => {
|
||||||
|
9 | await page.fill("[data-testid='notes-textarea']:visible", "Тестовая заметка");
|
||||||
|
10 | await expect(page.locator("[data-testid='notes-textarea']:visible")).toHaveValue("Тестовая заметка");
|
||||||
|
11 | });
|
||||||
|
12 |
|
||||||
|
13 | test("auto-save indicator appears", async ({ page }) => {
|
||||||
|
14 | await page.fill("[data-testid='notes-textarea']:visible", "Тест");
|
||||||
|
15 | await page.waitForTimeout(600);
|
||||||
|
16 | await expect(page.locator("[data-testid='notes-saved-indicator']:visible")).toBeVisible();
|
||||||
|
17 | });
|
||||||
|
18 |
|
||||||
|
19 | test("clear button empties notes", async ({ page }) => {
|
||||||
|
20 | await page.fill("[data-testid='notes-textarea']:visible", "Тестовая заметка");
|
||||||
|
21 | await page.click("[data-testid='notes-clear-btn']:visible");
|
||||||
|
22 | await expect(page.locator("[data-testid='notes-textarea']:visible")).toHaveValue("");
|
||||||
|
23 | });
|
||||||
|
24 |
|
||||||
|
25 | test("character counter updates", async ({ page }) => {
|
||||||
|
26 | await page.fill("[data-testid='notes-textarea']:visible", "12345");
|
||||||
|
27 | await expect(page.locator("[data-testid='notes-char-count']:visible")).toContainText("5");
|
||||||
|
28 | });
|
||||||
|
29 |
|
||||||
|
30 | test("mobile viewport renders correctly", async ({ page }) => {
|
||||||
|
31 | await page.setViewportSize({ width: 375, height: 812 });
|
||||||
|
32 | await expect(page.locator("[data-testid='notes-section']:visible")).toBeVisible();
|
||||||
|
33 | });
|
||||||
|
34 | });
|
||||||
|
35 |
|
||||||
|
```
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Instructions
|
||||||
|
|
||||||
|
- Following Playwright test failed.
|
||||||
|
- Explain why, be concise, respect Playwright best practices.
|
||||||
|
- Provide a snippet of code with the fix, if possible.
|
||||||
|
|
||||||
|
# Test info
|
||||||
|
|
||||||
|
- Name: dm/reference.spec.ts >> Open5e Reference >> tab switching works
|
||||||
|
- Location: tests/dm/reference.spec.ts:8:3
|
||||||
|
|
||||||
|
# Error details
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: browser.newContext: Target page, context or browser has been closed
|
||||||
|
Browser logs:
|
||||||
|
|
||||||
|
<launching> /home/emil/.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-edgeupdater --disable-extensions --disable-features=AvoidUnnecessaryBeforeUnloadCheckSync,BoundaryEventDispatchTracksNodeRemoval,DestroyProfileOnBrowserClose,DialMediaRouteProvider,GlobalMediaControls,HttpsUpgrades,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate,AutoDeElevate,RenderDocument,OptimizationHints,msForceBrowserSignIn,msEdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-BU4mcn --remote-debugging-pipe --no-startup-window
|
||||||
|
<launched> pid=1566421
|
||||||
|
[pid=1566421][err] [0515/214659.568038:WARNING:media/gpu/vaapi/vaapi_wrapper.cc:123] Should skip nVidia device named: nvidia-drm
|
||||||
|
[pid=1566421][err] [0515/214659.570324:WARNING:sandbox/policy/linux/sandbox_linux.cc:404] InitializeSandbox() called with multiple threads in process gpu-process.
|
||||||
|
[pid=1566421][err] [0515/214700.164452:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566421][err] [0515/214700.260928:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566421][err] [0515/214701.329816:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566421][err] [0515/214703.720377:INFO:CONSOLE:1182] "[vite] connecting...", source: http://localhost:4321/@vite/client (1182)
|
||||||
|
[pid=1566421][err] [0515/214704.139991:INFO:CONSOLE:1305] "[vite] connected.", source: http://localhost:4321/@vite/client (1305)
|
||||||
|
[pid=1566421][err] [0515/214704.734778:INFO:third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:941] Running WebRTC with a combined Network and Worker thread.
|
||||||
|
[pid=1566421] <gracefully close start>
|
||||||
|
```
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
getCorsHeaders,
|
||||||
|
createCorsResponse,
|
||||||
|
handleCorsPreflight,
|
||||||
|
jsonResponse,
|
||||||
|
} from "../src/lib/cors";
|
||||||
|
|
||||||
|
describe("CORS utility", () => {
|
||||||
|
describe("getCorsHeaders", () => {
|
||||||
|
it("returns headers for allowed origin randify.pro", () => {
|
||||||
|
const headers = getCorsHeaders("https://randify.pro");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("https://randify.pro");
|
||||||
|
expect(headers["Access-Control-Allow-Methods"]).toBe("GET, POST, OPTIONS");
|
||||||
|
expect(headers["Access-Control-Allow-Headers"]).toBe("Content-Type, Authorization");
|
||||||
|
expect(headers["Access-Control-Allow-Credentials"]).toBe("true");
|
||||||
|
expect(headers["Vary"]).toBe("Origin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns headers for allowed origin dm.randify.pro", () => {
|
||||||
|
const headers = getCorsHeaders("https://dm.randify.pro");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("https://dm.randify.pro");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns headers for allowed origin localhost:4321", () => {
|
||||||
|
const headers = getCorsHeaders("http://localhost:4321");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("http://localhost:4321");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty origin for disallowed host", () => {
|
||||||
|
const headers = getCorsHeaders("https://evil.com");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty origin for null origin", () => {
|
||||||
|
const headers = getCorsHeaders(null);
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty origin for invalid URL", () => {
|
||||||
|
const headers = getCorsHeaders("not-a-url");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleCorsPreflight", () => {
|
||||||
|
it("returns 204 with CORS headers for allowed origin", () => {
|
||||||
|
const response = handleCorsPreflight("https://randify.pro");
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://randify.pro");
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Methods")).toBe("GET, POST, OPTIONS");
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Credentials")).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 204 even for disallowed origin", () => {
|
||||||
|
const response = handleCorsPreflight("https://evil.com");
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCorsResponse", () => {
|
||||||
|
it("wraps a response with CORS headers", () => {
|
||||||
|
const response = createCorsResponse("hello", 200, "https://dm.randify.pro", {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://dm.randify.pro");
|
||||||
|
expect(response.headers.get("Content-Type")).toBe("text/plain");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("jsonResponse", () => {
|
||||||
|
it("serializes data and sets JSON content type", async () => {
|
||||||
|
const response = jsonResponse({ ok: true }, 200, "http://localhost:4321");
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("Content-Type")).toBe("application/json");
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("http://localhost:4321");
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockRequest(url: string, origin: string, method = "GET") {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
get(name: string) {
|
||||||
|
if (name.toLowerCase() === "origin") return origin;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DM API health route", () => {
|
||||||
|
it("GET returns 200 with CORS headers", async () => {
|
||||||
|
const { GET } = await import("../src/pages/api/dm/health");
|
||||||
|
const request = mockRequest("https://dm.randify.pro/api/dm/health", "https://randify.pro");
|
||||||
|
const response = await GET!({ request, url: new URL(request.url), ...({} as any) });
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://randify.pro");
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.status).toBe("ok");
|
||||||
|
expect(body.dm).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("OPTIONS returns 204 with CORS headers", async () => {
|
||||||
|
const { OPTIONS } = await import("../src/pages/api/dm/health");
|
||||||
|
const request = mockRequest("https://dm.randify.pro/api/dm/health", "https://dm.randify.pro", "OPTIONS");
|
||||||
|
const response = await OPTIONS!({ request, url: new URL(request.url), ...({} as any) });
|
||||||
|
expect(response.status).toBe(204);
|
||||||
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("https://dm.randify.pro");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("DM Sidebar", () => {
|
||||||
|
test("renders navigation sections when logged out", async ({ page }, testInfo) => {
|
||||||
|
if (testInfo.project.name === "mobile-chromium") test.skip();
|
||||||
|
await page.goto("/dm/");
|
||||||
|
const sidebar = page.locator("aside.w-full");
|
||||||
|
await expect(sidebar).toBeVisible();
|
||||||
|
await expect(sidebar.getByText("ИНСТРУМЕНТЫ", { exact: true })).toBeVisible();
|
||||||
|
await expect(sidebar.getByRole("link", { name: "Кубики" })).toBeVisible();
|
||||||
|
await expect(sidebar.getByRole("link", { name: "Инициатива" })).toBeVisible();
|
||||||
|
await expect(sidebar.getByRole("link", { name: "Справочник" })).toBeVisible();
|
||||||
|
await expect(sidebar.getByRole("link", { name: "Заметки" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not show user block when logged out", async ({ page }) => {
|
||||||
|
await page.goto("/dm/");
|
||||||
|
await expect(page.locator("[data-testid='sidebar-user-block']")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user block and tier badge have correct DOM structure", async ({ page }, testInfo) => {
|
||||||
|
if (testInfo.project.name === "mobile-chromium") test.skip();
|
||||||
|
await page.goto("/dm/");
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const aside = document.querySelector("aside.w-full");
|
||||||
|
if (!aside) return;
|
||||||
|
const block = document.createElement("div");
|
||||||
|
block.setAttribute("data-testid", "sidebar-user-block");
|
||||||
|
block.className = "flex items-center gap-3 mb-6 px-3 py-3 rounded-xl bg-[var(--bg-card)] border border-[var(--border-gold-strong)]";
|
||||||
|
block.innerHTML = `
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[var(--accent)] text-sm font-bold">T</div>
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<span class="text-sm font-medium text-[var(--text-primary)] truncate">Test User</span>
|
||||||
|
<span data-testid="tier-badge" data-tier="pro" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide bg-[var(--accent)] text-white">PRO</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
aside.prepend(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
const userBlock = page.locator("[data-testid='sidebar-user-block']");
|
||||||
|
await expect(userBlock).toBeVisible();
|
||||||
|
await expect(page.getByText("Test User")).toBeVisible();
|
||||||
|
|
||||||
|
const badge = page.locator("[data-testid='tier-badge']");
|
||||||
|
await expect(badge).toBeVisible();
|
||||||
|
await expect(badge).toHaveText("PRO");
|
||||||
|
await expect(badge).toHaveAttribute("data-tier", "pro");
|
||||||
|
});
|
||||||
|
});
|
||||||