feat(discovery): more work on discovery plus a few fixes
This commit is contained in:
@@ -22,14 +22,19 @@ import {createGuild, getGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils'
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {DiscoveryCategories} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import {DiscoveryCategories, type DiscoveryCategory} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {DiscoveryApplicationResponse} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import type {
|
||||
DiscoveryApplicationResponse,
|
||||
DiscoveryStatusResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
async function setGuildMemberCount(harness: ApiTestHarness, guildId: string, memberCount: number): Promise<void> {
|
||||
await createBuilder(harness, '').post(`/test/guilds/${guildId}/member-count`).body({member_count: memberCount}).execute();
|
||||
await createBuilder(harness, '')
|
||||
.post(`/test/guilds/${guildId}/member-count`)
|
||||
.body({member_count: memberCount})
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function applyForDiscovery(
|
||||
@@ -37,11 +42,11 @@ async function applyForDiscovery(
|
||||
token: string,
|
||||
guildId: string,
|
||||
description = 'A great community for testing discovery features',
|
||||
categoryId = DiscoveryCategories.GAMING,
|
||||
categoryId: DiscoveryCategory = DiscoveryCategories.GAMING,
|
||||
): Promise<DiscoveryApplicationResponse> {
|
||||
return createBuilder<DiscoveryApplicationResponse>(harness, token)
|
||||
.post(`/guilds/${guildId}/discovery`)
|
||||
.body({description, category_id: categoryId})
|
||||
.body({description, category_type: categoryId})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
@@ -98,7 +103,7 @@ describe('Discovery Application Lifecycle', () => {
|
||||
expect(application.guild_id).toBe(guild.id);
|
||||
expect(application.status).toBe('pending');
|
||||
expect(application.description).toBe('A great community for testing discovery features');
|
||||
expect(application.category_id).toBe(DiscoveryCategories.GAMING);
|
||||
expect(application.category_type).toBe(DiscoveryCategories.GAMING);
|
||||
expect(application.applied_at).toBeTruthy();
|
||||
expect(application.reviewed_at).toBeNull();
|
||||
expect(application.review_reason).toBeNull();
|
||||
@@ -111,13 +116,16 @@ describe('Discovery Application Lifecycle', () => {
|
||||
|
||||
await applyForDiscovery(harness, owner.token, guild.id);
|
||||
|
||||
const status = await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
const status = await createBuilder<DiscoveryStatusResponse>(harness, owner.token)
|
||||
.get(`/guilds/${guild.id}/discovery`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(status.guild_id).toBe(guild.id);
|
||||
expect(status.status).toBe('pending');
|
||||
expect(status.application).not.toBeNull();
|
||||
expect(status.application!.guild_id).toBe(guild.id);
|
||||
expect(status.application!.status).toBe('pending');
|
||||
expect(status.eligible).toBe(true);
|
||||
expect(status.min_member_count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should edit pending application description', async () => {
|
||||
@@ -134,7 +142,7 @@ describe('Discovery Application Lifecycle', () => {
|
||||
.execute();
|
||||
|
||||
expect(updated.description).toBe('Updated community description');
|
||||
expect(updated.category_id).toBe(DiscoveryCategories.GAMING);
|
||||
expect(updated.category_type).toBe(DiscoveryCategories.GAMING);
|
||||
});
|
||||
|
||||
test('should edit pending application category', async () => {
|
||||
@@ -146,11 +154,11 @@ describe('Discovery Application Lifecycle', () => {
|
||||
|
||||
const updated = await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.patch(`/guilds/${guild.id}/discovery`)
|
||||
.body({category_id: DiscoveryCategories.EDUCATION})
|
||||
.body({category_type: DiscoveryCategories.EDUCATION})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(updated.category_id).toBe(DiscoveryCategories.EDUCATION);
|
||||
expect(updated.category_type).toBe(DiscoveryCategories.EDUCATION);
|
||||
});
|
||||
|
||||
test('should withdraw pending application', async () => {
|
||||
@@ -165,10 +173,12 @@ describe('Discovery Application Lifecycle', () => {
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
const status = await createBuilder<DiscoveryStatusResponse>(harness, owner.token)
|
||||
.get(`/guilds/${guild.id}/discovery`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND, APIErrorCodes.DISCOVERY_APPLICATION_NOT_FOUND)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(status.application).toBeNull();
|
||||
});
|
||||
|
||||
test('should complete full lifecycle: apply → approve → verify feature → withdraw', async () => {
|
||||
@@ -207,11 +217,11 @@ describe('Discovery Application Lifecycle', () => {
|
||||
|
||||
await adminReject(harness, admin.token, guild.id, 'Needs more detail');
|
||||
|
||||
const status = await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
const status = await createBuilder<DiscoveryStatusResponse>(harness, owner.token)
|
||||
.get(`/guilds/${guild.id}/discovery`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(status.status).toBe('rejected');
|
||||
expect(status.application!.status).toBe('rejected');
|
||||
|
||||
const reapplication = await applyForDiscovery(
|
||||
harness,
|
||||
@@ -266,7 +276,7 @@ describe('Discovery Application Lifecycle', () => {
|
||||
'Valid description for this category',
|
||||
categoryId,
|
||||
);
|
||||
expect(application.category_id).toBe(categoryId);
|
||||
expect(application.category_type).toBe(categoryId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,11 +24,17 @@ import {HTTP_STATUS, TEST_IDS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {DiscoveryCategories} from '@fluxer/constants/src/DiscoveryConstants';
|
||||
import type {DiscoveryApplicationResponse} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import type {
|
||||
DiscoveryApplicationResponse,
|
||||
DiscoveryStatusResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
async function setGuildMemberCount(harness: ApiTestHarness, guildId: string, memberCount: number): Promise<void> {
|
||||
await createBuilder(harness, '').post(`/test/guilds/${guildId}/member-count`).body({member_count: memberCount}).execute();
|
||||
await createBuilder(harness, '')
|
||||
.post(`/test/guilds/${guildId}/member-count`)
|
||||
.body({member_count: memberCount})
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('Discovery Application Validation', () => {
|
||||
@@ -50,7 +56,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
const application = await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Small but active community', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Small but active community', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -64,7 +70,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'No members yet', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'No members yet', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.DISCOVERY_INSUFFICIENT_MEMBERS)
|
||||
.execute();
|
||||
});
|
||||
@@ -78,7 +84,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Valid description here', category_id: 99})
|
||||
.body({description: 'Valid description here', category_type: 99})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
@@ -90,7 +96,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Valid description here', category_id: -1})
|
||||
.body({description: 'Valid description here', category_type: -1})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
@@ -102,13 +108,13 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Valid description here', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Valid description here', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.patch(`/guilds/${guild.id}/discovery`)
|
||||
.body({category_id: 99})
|
||||
.body({category_type: 99})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
@@ -122,7 +128,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Too short', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Too short', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
@@ -134,7 +140,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'A'.repeat(301), category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'A'.repeat(301), category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
@@ -146,12 +152,12 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({category_id: DiscoveryCategories.GAMING})
|
||||
.body({category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject missing category_id', async () => {
|
||||
test('should reject missing category_type', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'No Cat Guild');
|
||||
await setGuildMemberCount(harness, guild.id, 1);
|
||||
@@ -172,13 +178,13 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'First application attempt', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'First application attempt', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Second application attempt', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Second application attempt', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.CONFLICT, APIErrorCodes.DISCOVERY_ALREADY_APPLIED)
|
||||
.execute();
|
||||
});
|
||||
@@ -192,7 +198,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Application to be approved', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Application to be approved', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -204,7 +210,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Trying to reapply while approved', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Trying to reapply while approved', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.CONFLICT, APIErrorCodes.DISCOVERY_ALREADY_APPLIED)
|
||||
.execute();
|
||||
});
|
||||
@@ -218,7 +224,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Should not be allowed', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Should not be allowed', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.FORBIDDEN, APIErrorCodes.MISSING_PERMISSIONS)
|
||||
.execute();
|
||||
});
|
||||
@@ -230,7 +236,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Owner applied for discovery', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Owner applied for discovery', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -248,7 +254,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Owner applied for discovery', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Owner applied for discovery', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -273,7 +279,7 @@ describe('Discovery Application Validation', () => {
|
||||
test('should require login to apply', async () => {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/guilds/${TEST_IDS.NONEXISTENT_GUILD}/discovery`)
|
||||
.body({description: 'No auth attempt', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'No auth attempt', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.UNAUTHORIZED)
|
||||
.execute();
|
||||
});
|
||||
@@ -302,14 +308,17 @@ describe('Discovery Application Validation', () => {
|
||||
});
|
||||
|
||||
describe('non-existent application', () => {
|
||||
test('should return error when getting status with no application', async () => {
|
||||
test('should return null application when none exists', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'No App Guild');
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
const status = await createBuilder<DiscoveryStatusResponse>(harness, owner.token)
|
||||
.get(`/guilds/${guild.id}/discovery`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND, APIErrorCodes.DISCOVERY_APPLICATION_NOT_FOUND)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(status.application).toBeNull();
|
||||
expect(status.min_member_count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should return error when editing non-existent application', async () => {
|
||||
@@ -344,7 +353,7 @@ describe('Discovery Application Validation', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'To be rejected for edit test', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'To be rejected for edit test', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ import type {
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
async function setGuildMemberCount(harness: ApiTestHarness, guildId: string, memberCount: number): Promise<void> {
|
||||
await createBuilder(harness, '').post(`/test/guilds/${guildId}/member-count`).body({member_count: memberCount}).execute();
|
||||
await createBuilder(harness, '')
|
||||
.post(`/test/guilds/${guildId}/member-count`)
|
||||
.body({member_count: memberCount})
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function applyAndApprove(
|
||||
@@ -45,7 +48,7 @@ async function applyAndApprove(
|
||||
): Promise<void> {
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, ownerToken)
|
||||
.post(`/guilds/${guildId}/discovery`)
|
||||
.body({description, category_id: categoryId})
|
||||
.body({description, category_type: categoryId})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -102,10 +105,7 @@ describe('Discovery Search and Join', () => {
|
||||
});
|
||||
|
||||
test('should require login to list categories', async () => {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.get('/discovery/categories')
|
||||
.expect(HTTP_STATUS.UNAUTHORIZED)
|
||||
.execute();
|
||||
await createBuilderWithoutAuth(harness).get('/discovery/categories').expect(HTTP_STATUS.UNAUTHORIZED).execute();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('Discovery Search and Join', () => {
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.name).toBe('Searchable Guild');
|
||||
expect(found!.description).toBe('A searchable community for all');
|
||||
expect(found!.category_id).toBe(DiscoveryCategories.GAMING);
|
||||
expect(found!.category_type).toBe(DiscoveryCategories.GAMING);
|
||||
});
|
||||
|
||||
test('should not return pending guilds in search results', async () => {
|
||||
@@ -160,7 +160,7 @@ describe('Discovery Search and Join', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Pending application guild', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Pending application guild', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
@@ -181,12 +181,26 @@ describe('Discovery Search and Join', () => {
|
||||
const owner1 = await createTestAccount(harness);
|
||||
const gamingGuild = await createGuild(harness, owner1.token, 'Gaming Community');
|
||||
await setGuildMemberCount(harness, gamingGuild.id, 10);
|
||||
await applyAndApprove(harness, owner1.token, admin.token, gamingGuild.id, 'All about gaming', DiscoveryCategories.GAMING);
|
||||
await applyAndApprove(
|
||||
harness,
|
||||
owner1.token,
|
||||
admin.token,
|
||||
gamingGuild.id,
|
||||
'All about gaming',
|
||||
DiscoveryCategories.GAMING,
|
||||
);
|
||||
|
||||
const owner2 = await createTestAccount(harness);
|
||||
const musicGuild = await createGuild(harness, owner2.token, 'Music Community');
|
||||
await setGuildMemberCount(harness, musicGuild.id, 10);
|
||||
await applyAndApprove(harness, owner2.token, admin.token, musicGuild.id, 'All about music', DiscoveryCategories.MUSIC);
|
||||
await applyAndApprove(
|
||||
harness,
|
||||
owner2.token,
|
||||
admin.token,
|
||||
musicGuild.id,
|
||||
'All about music',
|
||||
DiscoveryCategories.MUSIC,
|
||||
);
|
||||
|
||||
const searcher = await createTestAccount(harness);
|
||||
const results = await createBuilder<DiscoveryGuildListResponse>(harness, searcher.token)
|
||||
@@ -195,7 +209,7 @@ describe('Discovery Search and Join', () => {
|
||||
.execute();
|
||||
|
||||
for (const guild of results.guilds) {
|
||||
expect(guild.category_id).toBe(DiscoveryCategories.GAMING);
|
||||
expect(guild.category_type).toBe(DiscoveryCategories.GAMING);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -207,7 +221,14 @@ describe('Discovery Search and Join', () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, `Limit Test Guild ${i}`);
|
||||
await setGuildMemberCount(harness, guild.id, 10);
|
||||
await applyAndApprove(harness, owner.token, admin.token, guild.id, `Community number ${i} for testing`, DiscoveryCategories.GAMING);
|
||||
await applyAndApprove(
|
||||
harness,
|
||||
owner.token,
|
||||
admin.token,
|
||||
guild.id,
|
||||
`Community number ${i} for testing`,
|
||||
DiscoveryCategories.GAMING,
|
||||
);
|
||||
}
|
||||
|
||||
const searcher = await createTestAccount(harness);
|
||||
@@ -220,10 +241,7 @@ describe('Discovery Search and Join', () => {
|
||||
});
|
||||
|
||||
test('should require login to search', async () => {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.get('/discovery/guilds')
|
||||
.expect(HTTP_STATUS.UNAUTHORIZED)
|
||||
.execute();
|
||||
await createBuilderWithoutAuth(harness).get('/discovery/guilds').expect(HTTP_STATUS.UNAUTHORIZED).execute();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,7 +253,14 @@ describe('Discovery Search and Join', () => {
|
||||
|
||||
const admin = await createTestAccount(harness);
|
||||
await setUserACLs(harness, admin, ['admin:authenticate', 'discovery:review']);
|
||||
await applyAndApprove(harness, owner.token, admin.token, guild.id, 'Join this community', DiscoveryCategories.GAMING);
|
||||
await applyAndApprove(
|
||||
harness,
|
||||
owner.token,
|
||||
admin.token,
|
||||
guild.id,
|
||||
'Join this community',
|
||||
DiscoveryCategories.GAMING,
|
||||
);
|
||||
|
||||
const joiner = await createTestAccount(harness);
|
||||
await createBuilder(harness, joiner.token)
|
||||
@@ -266,7 +291,7 @@ describe('Discovery Search and Join', () => {
|
||||
|
||||
await createBuilder<DiscoveryApplicationResponse>(harness, owner.token)
|
||||
.post(`/guilds/${guild.id}/discovery`)
|
||||
.body({description: 'Pending but not yet approved', category_id: DiscoveryCategories.GAMING})
|
||||
.body({description: 'Pending but not yet approved', category_type: DiscoveryCategories.GAMING})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
|
||||
396
packages/api/src/guild/tests/GuildRoleReorder.test.tsx
Normal file
396
packages/api/src/guild/tests/GuildRoleReorder.test.tsx
Normal file
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
acceptInvite,
|
||||
addMemberRole,
|
||||
createChannelInvite,
|
||||
createGuild,
|
||||
createRole,
|
||||
getChannel,
|
||||
getRoles,
|
||||
updateRolePositions,
|
||||
} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
describe('Guild Role Reorder', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
describe('Owner Reordering', () => {
|
||||
test('should allow owner to reorder any roles', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
const roleC = await createRole(harness, owner.token, guild.id, {name: 'Role C'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleC.id, position: 3},
|
||||
{id: roleB.id, position: 2},
|
||||
{id: roleA.id, position: 1},
|
||||
]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const updatedA = roles.find((r) => r.id === roleA.id)!;
|
||||
const updatedB = roles.find((r) => r.id === roleB.id)!;
|
||||
const updatedC = roles.find((r) => r.id === roleC.id)!;
|
||||
|
||||
expect(updatedC.position).toBeGreaterThan(updatedB.position);
|
||||
expect(updatedB.position).toBeGreaterThan(updatedA.position);
|
||||
});
|
||||
|
||||
test('should allow owner to reverse all role positions', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleA.id, position: 2},
|
||||
{id: roleB.id, position: 1},
|
||||
]);
|
||||
|
||||
const rolesBefore = await getRoles(harness, owner.token, guild.id);
|
||||
const beforeA = rolesBefore.find((r) => r.id === roleA.id)!;
|
||||
const beforeB = rolesBefore.find((r) => r.id === roleB.id)!;
|
||||
expect(beforeA.position).toBeGreaterThan(beforeB.position);
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleB.id, position: 2},
|
||||
{id: roleA.id, position: 1},
|
||||
]);
|
||||
|
||||
const rolesAfter = await getRoles(harness, owner.token, guild.id);
|
||||
const afterA = rolesAfter.find((r) => r.id === roleA.id)!;
|
||||
const afterB = rolesAfter.find((r) => r.id === roleB.id)!;
|
||||
expect(afterB.position).toBeGreaterThan(afterA.position);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-Owner with MANAGE_ROLES', () => {
|
||||
test('should allow member with MANAGE_ROLES to reorder roles below their highest role', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const manager = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const managerRole = await createRole(harness, owner.token, guild.id, {
|
||||
name: 'Manager',
|
||||
permissions: Permissions.MANAGE_ROLES.toString(),
|
||||
});
|
||||
const lowRoleA = await createRole(harness, owner.token, guild.id, {name: 'Low A'});
|
||||
const lowRoleB = await createRole(harness, owner.token, guild.id, {name: 'Low B'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: managerRole.id, position: 4},
|
||||
{id: lowRoleA.id, position: 3},
|
||||
{id: lowRoleB.id, position: 2},
|
||||
]);
|
||||
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, manager.token, invite.code);
|
||||
await addMemberRole(harness, owner.token, guild.id, manager.userId, managerRole.id);
|
||||
|
||||
await updateRolePositions(harness, manager.token, guild.id, [
|
||||
{id: lowRoleB.id, position: 3},
|
||||
{id: lowRoleA.id, position: 2},
|
||||
]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const updatedA = roles.find((r) => r.id === lowRoleA.id)!;
|
||||
const updatedB = roles.find((r) => r.id === lowRoleB.id)!;
|
||||
expect(updatedB.position).toBeGreaterThan(updatedA.position);
|
||||
});
|
||||
|
||||
test('should reject reordering a role above the user highest role', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const manager = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const highRole = await createRole(harness, owner.token, guild.id, {name: 'High Role'});
|
||||
const managerRole = await createRole(harness, owner.token, guild.id, {
|
||||
name: 'Manager',
|
||||
permissions: Permissions.MANAGE_ROLES.toString(),
|
||||
});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: highRole.id, position: 4},
|
||||
{id: managerRole.id, position: 3},
|
||||
]);
|
||||
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, manager.token, invite.code);
|
||||
await addMemberRole(harness, owner.token, guild.id, manager.userId, managerRole.id);
|
||||
|
||||
await createBuilder(harness, manager.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: highRole.id, position: 1}])
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject reordering that indirectly shifts an unmanageable role', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const manager = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const highRole = await createRole(harness, owner.token, guild.id, {name: 'High Role'});
|
||||
const managerRole = await createRole(harness, owner.token, guild.id, {
|
||||
name: 'Manager',
|
||||
permissions: Permissions.MANAGE_ROLES.toString(),
|
||||
});
|
||||
const lowRole = await createRole(harness, owner.token, guild.id, {name: 'Low Role'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: highRole.id, position: 4},
|
||||
{id: managerRole.id, position: 3},
|
||||
{id: lowRole.id, position: 2},
|
||||
]);
|
||||
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, manager.token, invite.code);
|
||||
await addMemberRole(harness, owner.token, guild.id, manager.userId, managerRole.id);
|
||||
|
||||
await createBuilder(harness, manager.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: lowRole.id, position: 5}])
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should allow reordering multiple roles below highest when unmanageable roles stay in place', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const manager = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const highRole = await createRole(harness, owner.token, guild.id, {name: 'High Role'});
|
||||
const managerRole = await createRole(harness, owner.token, guild.id, {
|
||||
name: 'Manager',
|
||||
permissions: Permissions.MANAGE_ROLES.toString(),
|
||||
});
|
||||
const lowA = await createRole(harness, owner.token, guild.id, {name: 'Low A'});
|
||||
const lowB = await createRole(harness, owner.token, guild.id, {name: 'Low B'});
|
||||
const lowC = await createRole(harness, owner.token, guild.id, {name: 'Low C'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: highRole.id, position: 6},
|
||||
{id: managerRole.id, position: 5},
|
||||
{id: lowA.id, position: 4},
|
||||
{id: lowB.id, position: 3},
|
||||
{id: lowC.id, position: 2},
|
||||
]);
|
||||
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, manager.token, invite.code);
|
||||
await addMemberRole(harness, owner.token, guild.id, manager.userId, managerRole.id);
|
||||
|
||||
await updateRolePositions(harness, manager.token, guild.id, [
|
||||
{id: lowC.id, position: 4},
|
||||
{id: lowA.id, position: 3},
|
||||
{id: lowB.id, position: 2},
|
||||
]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const updatedA = roles.find((r) => r.id === lowA.id)!;
|
||||
const updatedB = roles.find((r) => r.id === lowB.id)!;
|
||||
const updatedC = roles.find((r) => r.id === lowC.id)!;
|
||||
expect(updatedC.position).toBeGreaterThan(updatedA.position);
|
||||
expect(updatedA.position).toBeGreaterThan(updatedB.position);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Permission Requirements', () => {
|
||||
test('should require MANAGE_ROLES permission to reorder roles', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const role = await createRole(harness, owner.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: role.id, position: 5}])
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject reorder from a non-guild member', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const role = await createRole(harness, owner.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: role.id, position: 5}])
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
test('should reject reordering @everyone role', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: guild.id, position: 5}])
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject reorder with invalid role ID', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.patch(`/guilds/${guild.id}/roles`)
|
||||
.body([{id: '999999999999999999', position: 1}])
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should accept reorder with no position changes (no-op)', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleA.id, position: 2},
|
||||
{id: roleB.id, position: 1},
|
||||
]);
|
||||
|
||||
const rolesBefore = await getRoles(harness, owner.token, guild.id);
|
||||
const beforeA = rolesBefore.find((r) => r.id === roleA.id)!;
|
||||
const beforeB = rolesBefore.find((r) => r.id === roleB.id)!;
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleA.id, position: 2},
|
||||
{id: roleB.id, position: 1},
|
||||
]);
|
||||
|
||||
const rolesAfter = await getRoles(harness, owner.token, guild.id);
|
||||
const afterA = rolesAfter.find((r) => r.id === roleA.id)!;
|
||||
const afterB = rolesAfter.find((r) => r.id === roleB.id)!;
|
||||
|
||||
expect(afterA.position).toBe(beforeA.position);
|
||||
expect(afterB.position).toBe(beforeB.position);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Position Assignment', () => {
|
||||
test('should keep @everyone at position 0 after reorder', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleB.id, position: 2},
|
||||
{id: roleA.id, position: 1},
|
||||
]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const everyone = roles.find((r) => r.id === guild.id)!;
|
||||
expect(everyone.position).toBe(0);
|
||||
});
|
||||
|
||||
test('should assign positions to all roles after reorder', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
const roleC = await createRole(harness, owner.token, guild.id, {name: 'Role C'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleA.id, position: 3},
|
||||
{id: roleB.id, position: 2},
|
||||
{id: roleC.id, position: 1},
|
||||
]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const nonEveryoneRoles = roles.filter((r) => r.id !== guild.id);
|
||||
|
||||
for (const role of nonEveryoneRoles) {
|
||||
expect(role.position).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
const positions = nonEveryoneRoles.map((r) => r.position);
|
||||
const uniquePositions = new Set(positions);
|
||||
expect(uniquePositions.size).toBe(nonEveryoneRoles.length);
|
||||
});
|
||||
|
||||
test('should correctly order roles with a partial position update', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
|
||||
const roleA = await createRole(harness, owner.token, guild.id, {name: 'Role A'});
|
||||
const roleB = await createRole(harness, owner.token, guild.id, {name: 'Role B'});
|
||||
const roleC = await createRole(harness, owner.token, guild.id, {name: 'Role C'});
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [
|
||||
{id: roleA.id, position: 3},
|
||||
{id: roleB.id, position: 2},
|
||||
{id: roleC.id, position: 1},
|
||||
]);
|
||||
|
||||
await updateRolePositions(harness, owner.token, guild.id, [{id: roleC.id, position: 4}]);
|
||||
|
||||
const roles = await getRoles(harness, owner.token, guild.id);
|
||||
const updatedA = roles.find((r) => r.id === roleA.id)!;
|
||||
const updatedB = roles.find((r) => r.id === roleB.id)!;
|
||||
const updatedC = roles.find((r) => r.id === roleC.id)!;
|
||||
|
||||
expect(updatedC.position).toBeGreaterThan(updatedA.position);
|
||||
expect(updatedA.position).toBeGreaterThan(updatedB.position);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user