Spaces:
Running
Running
Verify cross-roster admin analysis and log actor role
Browse files- app.py +2 -2
- tests/analysis-admin.test.ts +70 -0
app.py
CHANGED
|
@@ -250,8 +250,8 @@ app = FastAPI(title="Sporalize AI", lifespan=lifespan)
|
|
| 250 |
@app.exception_handler(httpx.HTTPStatusError)
|
| 251 |
async def backend_error(request: Request, error: httpx.HTTPStatusError):
|
| 252 |
status = error.response.status_code
|
| 253 |
-
logger.error("Analysis backend request failed: path=%s status=%s response=%s",
|
| 254 |
-
request.url.path, status, error.response.text[:500])
|
| 255 |
try:
|
| 256 |
backend_body = error.response.json()
|
| 257 |
except ValueError:
|
|
|
|
| 250 |
@app.exception_handler(httpx.HTTPStatusError)
|
| 251 |
async def backend_error(request: Request, error: httpx.HTTPStatusError):
|
| 252 |
status = error.response.status_code
|
| 253 |
+
logger.error("Analysis backend request failed: path=%s status=%s actor_is_admin=%s response=%s",
|
| 254 |
+
request.url.path, status, getattr(request.state, "is_admin", False), error.response.text[:500])
|
| 255 |
try:
|
| 256 |
backend_body = error.response.json()
|
| 257 |
except ValueError:
|
tests/analysis-admin.test.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import { createServer } from "node:http";
|
| 3 |
+
import type { AddressInfo } from "node:net";
|
| 4 |
+
import { test } from "node:test";
|
| 5 |
+
import express from "express";
|
| 6 |
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
| 7 |
+
|
| 8 |
+
test("an administrator can start analysis for another user's player", async () => {
|
| 9 |
+
Object.assign(process.env, {
|
| 10 |
+
NODE_ENV: "test",
|
| 11 |
+
FRONTEND_URL: "http://127.0.0.1:3000",
|
| 12 |
+
BACKEND_URL: "http://127.0.0.1:4000",
|
| 13 |
+
ANALYSIS_PROXY_SECRET: "test-analysis-proxy-secret-32-chars",
|
| 14 |
+
SUPABASE_URL: "http://127.0.0.1:54321",
|
| 15 |
+
SUPABASE_ANON_KEY: "test-anon-key",
|
| 16 |
+
SUPABASE_SERVICE_ROLE_KEY: "test-service-role-key",
|
| 17 |
+
});
|
| 18 |
+
const { analysisRouter } = await import("../src/analysis.js");
|
| 19 |
+
const adminId = "11111111-1111-4111-8111-111111111111";
|
| 20 |
+
const ownerId = "22222222-2222-4222-8222-222222222222";
|
| 21 |
+
const playerId = "33333333-3333-4333-8333-333333333333";
|
| 22 |
+
const clientId = "44444444-4444-4444-8444-444444444444";
|
| 23 |
+
const filters: [string, string][] = [];
|
| 24 |
+
const rpcCalls: { name: string; args: Record<string, unknown> }[] = [];
|
| 25 |
+
const db = {
|
| 26 |
+
from(table: string) {
|
| 27 |
+
assert.equal(table, "players");
|
| 28 |
+
const query = {
|
| 29 |
+
select() { return query; },
|
| 30 |
+
eq(column: string, value: string) { filters.push([column, value]); return query; },
|
| 31 |
+
async maybeSingle() { return { data: { player_id: playerId, user_id: ownerId }, error: null }; },
|
| 32 |
+
};
|
| 33 |
+
return query;
|
| 34 |
+
},
|
| 35 |
+
async rpc(name: string, args: Record<string, unknown>) {
|
| 36 |
+
rpcCalls.push({ name, args });
|
| 37 |
+
return { error: null };
|
| 38 |
+
},
|
| 39 |
+
} as unknown as SupabaseClient;
|
| 40 |
+
const secret = "test-analysis-proxy-secret-32-chars";
|
| 41 |
+
const app = express();
|
| 42 |
+
app.use("/internal/analysis", analysisRouter(db, secret));
|
| 43 |
+
const server = createServer(app);
|
| 44 |
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
| 45 |
+
try {
|
| 46 |
+
const port = (server.address() as AddressInfo).port;
|
| 47 |
+
const response = await fetch(`http://127.0.0.1:${port}/internal/analysis`, {
|
| 48 |
+
method: "POST",
|
| 49 |
+
headers: {
|
| 50 |
+
"content-type": "application/json",
|
| 51 |
+
"x-sporalize-proxy-secret": secret,
|
| 52 |
+
"x-sporalize-user-id": adminId,
|
| 53 |
+
"x-sporalize-admin": "true",
|
| 54 |
+
},
|
| 55 |
+
body: JSON.stringify({ clientId, playerId, name: "Admin analysis" }),
|
| 56 |
+
});
|
| 57 |
+
assert.equal(response.status, 200);
|
| 58 |
+
assert.deepEqual(await response.json(), {
|
| 59 |
+
session_id: clientId, player_id: playerId, user_id: ownerId,
|
| 60 |
+
by_admin: true, actor_is_admin: true,
|
| 61 |
+
});
|
| 62 |
+
assert.deepEqual(filters, [["player_id", playerId]]);
|
| 63 |
+
assert.deepEqual(rpcCalls, [{ name: "admin_create_analysis_session", args: {
|
| 64 |
+
p_session_id: clientId, p_user_id: ownerId, p_player_id: playerId,
|
| 65 |
+
p_name: "Admin analysis", p_path: `sessions/${clientId}`, p_by_admin: true,
|
| 66 |
+
} }]);
|
| 67 |
+
} finally {
|
| 68 |
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
| 69 |
+
}
|
| 70 |
+
});
|