This commit is contained in:
@@ -42,7 +42,7 @@ beforeEach(() => {
|
||||
enabled: true,
|
||||
guildId,
|
||||
channels: { t5: channelId, t6: '' },
|
||||
publishedServers: ['kino']
|
||||
excludedServers: []
|
||||
});
|
||||
server = {
|
||||
name: 'kino',
|
||||
@@ -68,7 +68,7 @@ function worker(rest: DiscordTransport, render = () => [message('Public board')]
|
||||
return new DiscordWorker({
|
||||
store,
|
||||
transport: () => rest,
|
||||
fleet: (settings) => (settings.publishedServers.includes(server.name) ? [server] : []),
|
||||
fleet: (settings) => (!settings.excludedServers.includes(server.name) ? [server] : []),
|
||||
render,
|
||||
now: () => now
|
||||
});
|
||||
@@ -337,7 +337,7 @@ describe('Discord one-time availability notifications', () => {
|
||||
const watch = store.subscribe(watchInput);
|
||||
const { rest, request } = transport((method, path) => {
|
||||
if (path === '/users/@me/channels')
|
||||
store.saveSettings({ ...store.settings(), publishedServers: [] });
|
||||
store.saveSettings({ ...store.settings(), excludedServers: [server.name] });
|
||||
});
|
||||
const instance = worker(rest, () => []);
|
||||
await instance.tick();
|
||||
|
||||
@@ -122,7 +122,7 @@ beforeEach(() => {
|
||||
enabled: true,
|
||||
guildId,
|
||||
channels: { t5: '444444444444444444', t6: '' },
|
||||
publishedServers: [name]
|
||||
excludedServers: []
|
||||
};
|
||||
vi.mocked(getDiscordSettings).mockImplementation(() => settings);
|
||||
vi.mocked(fleetSnapshot).mockReturnValue({ servers: [server()] } as ReturnType<
|
||||
@@ -137,7 +137,22 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('Discord publication boundary', () => {
|
||||
it('projects only opted-in servers and explicitly approved fields', () => {
|
||||
it('publishes current and newly added servers by default while respecting exclusions', () => {
|
||||
const original = server();
|
||||
const added = server({ name: 't5_zombie_theater_2' });
|
||||
expect(projectFleet([original], settings, now).map((item) => item.name)).toEqual([name]);
|
||||
expect(projectFleet([original, added], settings, now).map((item) => item.name)).toEqual([
|
||||
name,
|
||||
added.name
|
||||
]);
|
||||
expect(
|
||||
projectFleet([original, added], { ...settings, excludedServers: [name] }, now).map(
|
||||
(item) => item.name
|
||||
)
|
||||
).toEqual([added.name]);
|
||||
});
|
||||
|
||||
it('excludes unpublished servers and projects only explicitly approved fields', () => {
|
||||
const raw = {
|
||||
...server(),
|
||||
host: { id: 'internal-host' },
|
||||
@@ -147,7 +162,7 @@ describe('Discord publication boundary', () => {
|
||||
};
|
||||
const projected = projectFleet(
|
||||
[raw as unknown as FleetServer, server({ name: 'unpublished' })],
|
||||
settings,
|
||||
{ ...settings, excludedServers: ['unpublished'] },
|
||||
now
|
||||
);
|
||||
expect(projected).toHaveLength(1);
|
||||
@@ -195,17 +210,54 @@ describe('Discord publication boundary', () => {
|
||||
const projected = projectFleet([server(change)], settings, now)[0];
|
||||
expect(projected).toMatchObject({ state, players });
|
||||
if (players === null)
|
||||
expect(projected).toMatchObject({ capacity: null, round: null, playerNames: [] });
|
||||
expect(projected).toMatchObject({ capacity: 4, round: null, playerNames: [] });
|
||||
});
|
||||
|
||||
it('never treats stale or previous-process capacity as an available space', () => {
|
||||
it('uses the four-player lobby limit when current capacity is unavailable', () => {
|
||||
for (const change of [
|
||||
{ capacity_observed_at: new Date(now - 100000).toISOString() },
|
||||
{ telemetry_after: new Date(now - 5000).toISOString() }
|
||||
{ capacity: null, capacity_observed_at: null },
|
||||
{ capacity: 0 },
|
||||
{ capacity: 8, capacity_observed_at: new Date(now - 100000).toISOString() },
|
||||
{
|
||||
capacity: 8,
|
||||
capacity_observed_at: new Date(now - 20000).toISOString(),
|
||||
telemetry_after: new Date(now - 15000).toISOString()
|
||||
}
|
||||
]) {
|
||||
const projected = projectFleet([server(change)], settings, now);
|
||||
expect(projected[0].capacity).toBeNull();
|
||||
expect(projected[0].capacity).toBe(4);
|
||||
expect(filterServers(projected, { status: 'space' })).toEqual(projected);
|
||||
for (const message of [
|
||||
...renderBoard('t5', projected),
|
||||
renderBrowser(projected),
|
||||
renderServer(projected[0])
|
||||
]) {
|
||||
expect(messageText(message)).toContain('2/4 players');
|
||||
expect(messageText(message)).not.toContain('2/?');
|
||||
}
|
||||
}
|
||||
const full = projectFleet([server({ last_players: 4, capacity: null })], settings, now);
|
||||
expect(full[0]).toMatchObject({ capacity: 4, state: 'full' });
|
||||
expect(filterServers(full, { status: 'space' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses a current measured capacity when available', () => {
|
||||
const projected = projectFleet([server({ capacity: 8, last_players: 4 })], settings, now);
|
||||
expect(projected[0]).toMatchObject({ capacity: 8, state: 'in-progress' });
|
||||
expect(messageText(renderServer(projected[0]))).toContain('4/8 players');
|
||||
expect(filterServers(projected, { status: 'space' })).toEqual(projected);
|
||||
});
|
||||
|
||||
it('never treats stale or previous-process occupancy as an available space', () => {
|
||||
for (const change of [
|
||||
{ last_seen_at: new Date(now - 100000).toISOString() },
|
||||
{ health: 'stale' as const },
|
||||
{ telemetry_after: new Date(now - 5000).toISOString() }
|
||||
]) {
|
||||
const projected = projectFleet([server({ ...change, capacity: null })], settings, now);
|
||||
expect(projected[0]).toMatchObject({ capacity: 4, players: null });
|
||||
expect(filterServers(projected, { status: 'space' })).toEqual([]);
|
||||
expect(messageText(renderServer(projected[0]))).toContain('Players unknown');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -218,12 +270,11 @@ describe('Discord publication boundary', () => {
|
||||
map_slug: 'town',
|
||||
telemetry: undefined
|
||||
});
|
||||
const setup = { ...settings, publishedServers: [town.name] };
|
||||
expect(projectFleet([town], setup, now)[0]).toMatchObject({
|
||||
expect(projectFleet([town], settings, now)[0]).toMatchObject({
|
||||
map: 'Town Survival',
|
||||
mapKey: 't6:zm_transit:town'
|
||||
});
|
||||
expect(projectFleet([{ ...town, last_map: 'zm_tomb' }], setup, now)[0]).toMatchObject({
|
||||
expect(projectFleet([{ ...town, last_map: 'zm_tomb' }], settings, now)[0]).toMatchObject({
|
||||
map: 'Origins',
|
||||
mapKey: 't6:zm_tomb:tomb'
|
||||
});
|
||||
@@ -355,7 +406,7 @@ describe('Discord interaction isolation', () => {
|
||||
expect(result.type).toBe(4);
|
||||
expect(result.data.flags).toBe(32832);
|
||||
expect(messageText(result.data)).toContain('Round 28');
|
||||
settings.publishedServers = [];
|
||||
settings.excludedServers = [name];
|
||||
expect(
|
||||
messageText(
|
||||
(
|
||||
@@ -400,7 +451,7 @@ describe('Discord interaction isolation', () => {
|
||||
});
|
||||
expect(messageText(result.data)).toContain('one DM');
|
||||
vi.mocked(subscribeToMap).mockClear();
|
||||
settings.publishedServers = [];
|
||||
settings.excludedServers = [name];
|
||||
await handleDiscordInteraction(
|
||||
interaction({ custom_id: `ec:w:space:${discordServerKey('t5:zombie_theater')}` }, 3)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { defaultDiscordSettings } from '../../src/lib/discord.js';
|
||||
import { migrateDiscord } from '../../src/lib/server/discord/schema.js';
|
||||
import { migrateDiscordPublicationDefaults } from '../../src/lib/server/discord/publication-defaults.js';
|
||||
import { DiscordStore } from '../../src/lib/server/discord/store.js';
|
||||
|
||||
let handle: Database.Database;
|
||||
let store: DiscordStore;
|
||||
beforeEach(() => {
|
||||
handle = new Database(':memory:');
|
||||
migrateDiscord(handle);
|
||||
handle.exec(`
|
||||
CREATE TABLE servers (name TEXT PRIMARY KEY, destroyed_at TEXT);
|
||||
INSERT INTO servers VALUES ('kino', NULL), ('town', NULL), ('retired', '2026-10-18');
|
||||
`);
|
||||
store = new DiscordStore(handle);
|
||||
});
|
||||
afterEach(() => handle.close());
|
||||
|
||||
function legacySettings(publishedServers: string[]) {
|
||||
const value: Record<string, unknown> = { ...defaultDiscordSettings, publishedServers };
|
||||
delete value.excludedServers;
|
||||
handle
|
||||
.prepare('INSERT INTO discord_configuration(id,value) VALUES(1,?)')
|
||||
.run(JSON.stringify(value));
|
||||
}
|
||||
|
||||
describe('Discord publication defaults upgrade', () => {
|
||||
it('includes all servers on new installations without creating configuration', () => {
|
||||
migrateDiscordPublicationDefaults(handle);
|
||||
expect(store.settings()).toEqual(defaultDiscordSettings);
|
||||
expect(store.settings().excludedServers).toEqual([]);
|
||||
expect(handle.prepare('SELECT * FROM discord_configuration').all()).toEqual([]);
|
||||
});
|
||||
|
||||
it('replaces the previously empty default selection without enabling Discord or optional sharing', () => {
|
||||
legacySettings([]);
|
||||
migrateDiscordPublicationDefaults(handle);
|
||||
expect(store.settings()).toEqual(defaultDiscordSettings);
|
||||
const value = (
|
||||
handle.prepare('SELECT value FROM discord_configuration').get() as { value: string }
|
||||
).value;
|
||||
expect(JSON.parse(value)).not.toHaveProperty('publishedServers');
|
||||
});
|
||||
|
||||
it('preserves existing nonempty selections as exclusions, and retains later opt-outs on replay', () => {
|
||||
legacySettings(['kino']);
|
||||
migrateDiscordPublicationDefaults(handle);
|
||||
expect(store.settings().excludedServers).toEqual(['town']);
|
||||
handle.exec("INSERT INTO servers VALUES ('moon', NULL)");
|
||||
expect(store.settings().excludedServers).not.toContain('moon');
|
||||
store.saveSettings({ ...store.settings(), excludedServers: ['kino', 'town'] });
|
||||
const before = handle.prepare('SELECT * FROM discord_configuration').all();
|
||||
migrateDiscordPublicationDefaults(handle);
|
||||
expect(handle.prepare('SELECT * FROM discord_configuration').all()).toEqual(before);
|
||||
});
|
||||
});
|
||||
@@ -199,9 +199,10 @@ describe('Discord signed webhook boundary', () => {
|
||||
});
|
||||
|
||||
describe('Discord configuration and access', () => {
|
||||
it('defaults to no publication and validates IDs and an explicit bounded allowlist', () => {
|
||||
it('defaults to all servers with sharing disabled and validates bounded exclusions', () => {
|
||||
expect(validateDiscordSettings(defaultDiscordSettings)).toEqual(defaultDiscordSettings);
|
||||
expect(defaultDiscordSettings.enabled).toBe(false);
|
||||
expect(defaultDiscordSettings.excludedServers).toEqual([]);
|
||||
expect(defaultDiscordSettings.showPlayers).toBe(false);
|
||||
expect(defaultDiscordSettings.showAddresses).toBe(false);
|
||||
expect(defaultDiscordSettings.leaderboardEnabled).toBe(false);
|
||||
@@ -214,20 +215,20 @@ describe('Discord configuration and access', () => {
|
||||
expect(() =>
|
||||
validateDiscordSettings({
|
||||
...defaultDiscordSettings,
|
||||
publishedServers: ['../../secrets']
|
||||
excludedServers: ['../../secrets']
|
||||
})
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
validateDiscordSettings({
|
||||
...defaultDiscordSettings,
|
||||
publishedServers: Array(101).fill('t5_map_1')
|
||||
excludedServers: Array(101).fill('t5_map_1')
|
||||
})
|
||||
).toThrow();
|
||||
expect(
|
||||
validateDiscordSettings({
|
||||
...defaultDiscordSettings,
|
||||
publishedServers: ['t5_map_2', 't5_map_1', 't5_map_1']
|
||||
}).publishedServers
|
||||
excludedServers: ['t5_map_2', 't5_map_1', 't5_map_1']
|
||||
}).excludedServers
|
||||
).toEqual(['t5_map_1', 't5_map_2']);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user