forked from wangziqi/gongxue-base
feat: use preset avatars and disable leaderboard by default
This commit is contained in:
@@ -221,6 +221,29 @@ async function latestVideoPlayEventByToken(playToken) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setTenantFeatureFlag(tenantId, flag, enabled) {
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.tenant_settings (tenant_id, feature_flags, admin_feature_flags, public_config)
|
||||
values ($1, jsonb_build_object($2::text, $3::boolean), '{}'::jsonb, '{}'::jsonb)
|
||||
on conflict (tenant_id)
|
||||
do update set feature_flags = jsonb_set(
|
||||
coalesce(public.tenant_settings.feature_flags, '{}'::jsonb),
|
||||
array[$2::text],
|
||||
to_jsonb($3::boolean),
|
||||
true
|
||||
),
|
||||
updated_at = now()
|
||||
`,
|
||||
[tenantId, flag, enabled],
|
||||
);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
function getFreePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
@@ -2711,6 +2734,39 @@ async function testProfile() {
|
||||
assert.equal(payload.item?.userId, USER_ID, 'profile should belong to smoke user');
|
||||
assert.ok(payload.item?.stats?.vocabulary?.totalWords >= 1, 'profile should include vocabulary stats');
|
||||
assert.ok(Array.isArray(payload.item?.recentPractices), 'profile should include recent practices');
|
||||
assert.equal(payload.item?.avatarPreset, 'male', 'profile should default to the male preset avatar');
|
||||
assert.equal(payload.item?.avatar?.preset, 'male', 'profile avatar object should expose preset');
|
||||
assert.ok(payload.item?.avatar?.displayUrl?.includes('/assets/avatars/default-male.svg'), 'profile should expose the default male avatar asset path');
|
||||
|
||||
const directAvatarRejected = await request('/api/profile/me', {
|
||||
method: 'PATCH',
|
||||
body: { avatarUrl: 'https://example.test/not-allowed-avatar.png' },
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(
|
||||
directAvatarRejected.code,
|
||||
'PROFILE_AVATAR_URL_DIRECT_UPDATE_REJECTED',
|
||||
'students must not set arbitrary avatar URLs directly',
|
||||
);
|
||||
|
||||
const invalidAvatarPatch = await request('/api/profile/me', {
|
||||
method: 'PATCH',
|
||||
body: { avatarPreset: 'custom' },
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidAvatarPatch.code, 'INVALID_AVATAR_PRESET', 'profile must reject unknown avatar presets');
|
||||
|
||||
const avatarPresetUpdated = await request('/api/profile/me', {
|
||||
method: 'PATCH',
|
||||
body: { avatarPreset: 'female' },
|
||||
});
|
||||
assert.equal(avatarPresetUpdated.item?.avatarPreset, 'female', 'student should update preset avatar choice');
|
||||
const profileAfterAvatarPreset = await request('/api/profile/me');
|
||||
assert.equal(profileAfterAvatarPreset.item?.avatar?.preset, 'female', 'profile should expose updated preset avatar');
|
||||
assert.ok(
|
||||
profileAfterAvatarPreset.item?.avatar?.displayUrl?.includes('/assets/avatars/default-female.svg'),
|
||||
'profile should expose the default female avatar asset path',
|
||||
);
|
||||
|
||||
const countdowns = await request('/api/profile/exam-countdowns');
|
||||
const profileExamDate = countdowns.items?.find(item => item.id === ids.examDate);
|
||||
@@ -2865,6 +2921,14 @@ async function testProfile() {
|
||||
}
|
||||
|
||||
async function testLearningLeaderboard() {
|
||||
const disabled = await request('/api/learning/leaderboard', {
|
||||
query: { metric: 'questions', period: 'all', limit: 10 },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(disabled.code, 'LEADERBOARD_DISABLED', 'leaderboard should be disabled by default for tenants');
|
||||
|
||||
await setTenantFeatureFlag(MAIN_TENANT_ID, 'enableLeaderboard', true);
|
||||
|
||||
const questions = await request('/api/learning/leaderboard', {
|
||||
query: { metric: 'questions', period: 'all', limit: 10 },
|
||||
});
|
||||
@@ -2911,6 +2975,13 @@ async function testLearningLeaderboard() {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(crossTenantDenied.code, 'AUTH_TENANT_MISMATCH', 'leaderboard must reject trusted session cross-tenant access');
|
||||
|
||||
await setTenantFeatureFlag(MAIN_TENANT_ID, 'enableLeaderboard', false);
|
||||
const disabledAfterRestore = await request('/api/learning/leaderboard', {
|
||||
query: { metric: 'questions', period: 'all', limit: 10 },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(disabledAfterRestore.code, 'LEADERBOARD_DISABLED', 'leaderboard feature flag should be restorable after tests');
|
||||
}
|
||||
|
||||
async function testScoreline() {
|
||||
|
||||
@@ -14,6 +14,7 @@ const DURATION_SECONDS = envNumber('PERF_DURATION_SECONDS', 15);
|
||||
const CONCURRENCY = envNumber('PERF_CONCURRENCY', 6);
|
||||
const RAMP_SECONDS = envNumber('PERF_RAMP_SECONDS', 3, { allowZero: true });
|
||||
const INCLUDE_WRITES = envBool('PERF_INCLUDE_WRITES', false);
|
||||
const INCLUDE_LEADERBOARD = envBool('PERF_INCLUDE_LEADERBOARD', false);
|
||||
const START_PORT = Number(process.env.PERF_API_PORT || 0) || 0;
|
||||
const TENANT_CODE = process.env.PERF_TENANT_CODE || 'master';
|
||||
const QUESTION_LIMIT = envNumber('PERF_QUESTION_LIMIT', 20);
|
||||
@@ -381,8 +382,16 @@ function createEndpointCatalog(context) {
|
||||
{ name: 'catalog.content_entries', weight: 10, method: 'GET', path: '/api/catalog/content-entries', query: { entryType: 'question_practice' } },
|
||||
{ name: 'learning.stats', weight: 5, method: 'GET', path: '/api/learning/stats' },
|
||||
{ name: 'learning.trend', weight: 4, method: 'GET', path: '/api/learning/trend', query: { days: 14 } },
|
||||
{ name: 'leaderboard.questions_7d', weight: 3, method: 'GET', path: '/api/learning/leaderboard', query: { metric: 'questions', period: '7d', limit: 20 } },
|
||||
];
|
||||
if (INCLUDE_LEADERBOARD) {
|
||||
endpoints.push({
|
||||
name: 'leaderboard.questions_7d',
|
||||
weight: 3,
|
||||
method: 'GET',
|
||||
path: '/api/learning/leaderboard',
|
||||
query: { metric: 'questions', period: '7d', limit: 20 },
|
||||
});
|
||||
}
|
||||
|
||||
if (context.entry) {
|
||||
endpoints.push(
|
||||
|
||||
Reference in New Issue
Block a user