diff --git a/package.json b/package.json index 317f3b6..1b1a924 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "build": "npx tsc", "startServer": "node out/serverMain.js", "startClient": "node out/clientMain.js", - "startProcess": "node out/processMain.js", "documentation": "redocly build-docs .\\docs\\v1.yml --output=docs\\index.html", - "test": "jest" + "test": "jest --runInBand" } } \ No newline at end of file diff --git a/src/cache/cacheUtils.ts b/src/cache/cacheUtils.ts deleted file mode 100644 index 0dd17d4..0000000 --- a/src/cache/cacheUtils.ts +++ /dev/null @@ -1,97 +0,0 @@ -import cache from './redis' -/** - * Shows the data for one weapon - * @param weapon - * @param server - * @param player - * @returns - */ -export async function getWeaponReport( - weapon: string, - server?: number, - player?: string -) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const playerPrefix = player ? `players:${player}:` : '' - const cacheLocation = serverPrefix + playerPrefix + `weapons:` + weapon - const data = await cache.HGETALL(cacheLocation) - delete data.last_entry - return data -} - -export function getWeaponSet(server?: number, player?: string) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const playerPrefix = player ? `players:${player}:` : '' - const cacheLocation = serverPrefix + playerPrefix + `weapons` - return cache.SMEMBERS(cacheLocation) -} - -/** - * Show the processed list of weapons - * @param server - * @param player - * @returns string - */ -export async function getWeaponList(server?: string, player?: string) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const playerPrefix = player ? `players:${player}:` : '' - return JSON.parse( - (await cache.GET(serverPrefix + playerPrefix + `weapons:processedList`)) || - '{}' - ) -} - -/** - * Show processed data for a player - * @param player - * @param server - * @param weapon - * @returns - */ -export async function getPlayerReport( - player: string, - server?: number, - weapon?: string -) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const weaponPrefix = weapon ? `weapons:${weapon}:` : '' - const cacheLocation = serverPrefix + weaponPrefix + `players:` + player - const data = await cache.HGETALL(cacheLocation) - delete data.last_entry - return data -} - -/** - * Shows processed list of players - * @param server - * @param weapon - * @returns string - */ -export async function getPlayerList(server?: number, weapon?: string) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const weaponPrefix = weapon ? `weapons:${weapon}:` : '' - - return JSON.parse( - (await cache.GET(serverPrefix + weaponPrefix + `players:processedList`)) || - '{}' - ) -} - -export function getPlayerSet(server?: number, weapon?: string) { - const serverPrefix = !isNaN(Number(server?.toString())) - ? `servers:${server}:` - : '' - const weaponPrefix = weapon ? `weapons:${weapon}:` : '' - const cacheLocation = serverPrefix + weaponPrefix + `players` - return cache.SMEMBERS(cacheLocation) -} diff --git a/src/cache/redis.ts b/src/cache/redis.ts deleted file mode 100644 index 63c61cd..0000000 --- a/src/cache/redis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createClient } from 'redis' - -const client = createClient({ - url: process.env.REDIS_URL, - username: process.env.REDIS_USER, - password: process.env.REDIS_PASSWORD -}) - -client.on('error', (err) => console.log('Redis Client Error', err)) - -export function cacheReady() { - return client.connect() -} -export default client - -//await client.set('key', 'value'); -//const value = await client.get('key'); -//await client.disconnect(); diff --git a/src/client/client.ts b/src/client/client.ts index b858835..4e0f291 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,101 +1,94 @@ import { Router } from 'express' import { param, query } from 'express-validator' import { validateErrors } from '../common' -import db from '../db/db' -import { - getWeaponList, - getWeaponReport, - getPlayerReport, - getPlayerList -} from '../cache/cacheUtils' +import { allData } from '../process/process' +import { getHostList } from '../db/db' + const router = Router() //timeout middleware ? router.get('/*', (req, res, next) => { next() }) -router.get( - '/weapons/:weaponId', - param(['weaponId']).exists().isString(), - query(['player', 'server']).optional().toInt().isInt(), - validateErrors, - async (req, res) => { - const data = await getWeaponReport( - req.params.weaponId, - Number(req.query.server), - req.query.player?.toString() - ) - res.status(200).send(data) - } -) - -router.get( - '/weapons/', - query(['player', 'server']).optional().toInt().isInt(), - validateErrors, - async (req, res) => { - const data = await getWeaponList( - req.query.server?.toString(), - req.query.player?.toString() - ) - res.status(200).send(data) - } -) - -router.get( - '/players/:playerId', - param(['playerId']).exists().toInt().isInt(), - query(['server']).optional().toInt().isInt(), - query('weapon').optional().isString(), - validateErrors, - async (req, res) => { - const data = await getPlayerReport( - req.params.playerId, - Number(req.query.server), - req.query.weapon?.toString() - ) - /* - if (!req.query.weapon) { - const weapons = await getWeaponList( - req.query.server?.toString(), - req.params.playerId - ) - data.weapons = weapons - } else { - data.weapons = (await getWeaponReport( - req.query.weapon.toString(), - Number(req.query.server) || undefined, - req.params.playerId - )) as any - }*/ - res.status(200).send(data) - } -) - -router.get( - '/players/', - query(['server']).optional().toInt().isInt(), - query('weapon').optional().isString(), - validateErrors, - async (req, res) => { - const data = await getPlayerList( - Number(req.query.server), - req.query.weapon?.toString() - ) - res.status(200).send(data) - } -) - -//router.get('/maps/', (req, res, next) => {}) - -router.get('/servers/', async (req, res) => { - const data = await db.selectFrom('server').selectAll().execute() - res.status(200).send( - data.map((e) => { - return { name: e.name, id: e.id, description: e.description } - }) +function filters(e: any, query: any) { + return ( + (query.player ? query.player == e.attacker_id : true) && + (query.server ? query.server == e.servername : true) && + (query.host ? Number(query.host) == e.host : true) && + (query.map ? query.map == e.map : true) && + (query.weapon ? query.weapon == e.cause_of_death : true) && + (query.gamemode ? query.gamemode == e.game_mode : true) ) -}) -//router.get('/servers/:serverId/', (req, res, next) => {}) +} + +router.get('/hosts', + async (req, res) => { + const result = (await getHostList()) + const data: { [key: number]: string } = {} + result.forEach(e => data[Number(e.id)] = e.name) + res.status(200).send(data) + }) + +router.get( + '/:dataType', + param('dataType') + .custom( + (e) => e == 'weapons' || e == 'players' || e == 'maps' || e == 'servers' + ) + .withMessage('Only weapons, players, maps or servers are valid paths'), + query(['player', 'host']).optional().toInt().isInt(), + query(['server', 'map', 'weapon', 'gamemode']).optional().isString(), + validateErrors, + (req, res) => { + const data: { + [key: string]: { + deaths: number + kills: number + max_distance: number + total_distance: number + username?: string + host?: number + } + } = {} + let index: 'cause_of_death' | 'attacker_id' | 'map' | 'servername' + switch (req.params.dataType) { + case 'weapons': + index = 'cause_of_death' + break + case 'players': + index = 'attacker_id' + break + case 'maps': + index = 'map' + break + case 'servers': + index = 'servername' + break + default: + return res.status(400).send() + } + allData + .filter((e) => filters(e, req.query)) + .forEach((e) => { + if (!data[e[index]]) + data[e[index]] = { + deaths: 0, + kills: 0, + max_distance: 0, + total_distance: 0 + } + if (index === 'attacker_id') data[e[index]].username = e.attacker_name + if (index === 'servername') data[e[index]].host = e.host + data[e[index]].deaths += Number(e.deaths) + data[e[index]].kills += Number(e.kills) + data[e[index]].total_distance += Number(e.total_distance) + data[e[index]].max_distance = Math.max( + data[e[index]].max_distance, + Number(e.max_distance) + ) + }) + res.status(200).send(data) + } +) export default router diff --git a/src/client/process.ts b/src/client/process.ts deleted file mode 100644 index 6df4ecc..0000000 --- a/src/client/process.ts +++ /dev/null @@ -1,227 +0,0 @@ -import db from '../db/db' -const { count, max, sum } = db.fn -import client from '../cache/redis' -import { createWeaponJson } from './process/processWeapon' -import { createPlayerJson } from './process/processPlayer' - -/** - * Starts the process of populating the REDIS database globally - * @returns - */ -async function processAll() { - console.log('Starting data calculation...') - const timeStart = new Date() - - await Promise.all([processGlobalStats(), processServerStats()]) - console.log( - 'Data calculation finished. Took + ' + - Math.abs(new Date().getTime() - timeStart.getTime()) / 1000 + - ' seconds' - ) - if (process.env.ENVIRONMENT == 'production') { - return - } - setTimeout(processAll, 3600000) -} - -async function processGlobalStats() { - let promises: Promise[] = [] - //Global kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance')]) - .execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance }) => { - transaction = processGlobalData({ kills, max_distance, total_distance }, transaction) - }) - return transaction.exec() - })) - - //Global weapon kill - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'cause_of_death']) - .groupBy('cause_of_death').execute().then(async (data) => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, cause_of_death }) => { - transaction = processWeaponData({ kills, max_distance, total_distance, cause_of_death }, transaction) - }) - await transaction.exec() - })) - - //Player weapon kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'cause_of_death', 'attacker_id']) - .groupBy(['cause_of_death', 'attacker_id']).execute().then(async (data) => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, cause_of_death, attacker_id }) => { - transaction = processWeaponData({ kills, max_distance, total_distance, cause_of_death, attacker_id }, transaction) - }) - await transaction.exec() - })) - //Player weapon deaths - promises.push(db.selectFrom('kill').select([count('id').as('deaths'), 'victim_id', 'cause_of_death']) - .groupBy(['victim_id', 'cause_of_death']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ deaths, victim_id, cause_of_death }) => { - transaction = transaction.HSET(`weapons:${cause_of_death}:players:${victim_id}`, "deaths", deaths.toString()) - }) - return transaction.exec() - })) - - //Global player kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'attacker_id']) - .groupBy('attacker_id').execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, attacker_id }) => { - processPlayerData({ kills, max_distance, total_distance, attacker_id }, transaction) - }) - return transaction.exec() - }) - ) - //Global player deaths - promises.push(db.selectFrom('kill').select([count('id').as('deaths'), 'victim_id']) - .groupBy('victim_id').execute().then(data => { - let transaction = client.multi() - data.forEach(({ deaths, victim_id }) => { - transaction = transaction.HSET('players:' + victim_id, "deaths", deaths.toString()) - }) - return transaction.exec() - })) - - await Promise.all(promises) - promises = []; - promises.push(createWeaponJson()); - promises.push(createPlayerJson()); - (await client.sMembers('players')).forEach((player: string) => { - promises.push(createWeaponJson(undefined, player)) - }); - (await client.sMembers('weapons')).forEach((weapon: string) => { - promises.push(createPlayerJson(undefined, weapon)) - }); - await Promise.all(promises) -} - -async function processServerStats() { - let promises: Promise[] = [] - //Server global kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'server']) - .groupBy(['server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, server }) => { - transaction = processGlobalData({ kills, max_distance, total_distance, server }, transaction) - }) - return transaction.exec() - })) - //Server weapon kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'cause_of_death', 'server']) - .groupBy(['cause_of_death', 'server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, cause_of_death, server }) => { - transaction = processWeaponData({ kills, max_distance, total_distance, cause_of_death, server }, transaction) - }) - return transaction.exec() - })) - - //Server Player weapon kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'cause_of_death', 'attacker_id', 'server']) - .groupBy(['cause_of_death', 'attacker_id', 'server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, cause_of_death, attacker_id, server }) => { - transaction = processWeaponData({ kills, max_distance, total_distance, cause_of_death, attacker_id, server }, transaction) - }) - return transaction.exec() - })) - //Server Player weapon deaths - promises.push(db.selectFrom('kill').select([count('id').as('deaths'), 'victim_id', 'cause_of_death', 'server']) - .groupBy(['victim_id', 'cause_of_death', 'server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ deaths, victim_id, cause_of_death, server }) => { - transaction = transaction.HSET(`servers:${server}:weapons:${cause_of_death}:players:${victim_id}`, "deaths", deaths.toString()) - }) - return transaction.exec() - })) - - //server player kills - promises.push(db.selectFrom('kill') - .select([count('id').as('kills'), max('distance').as('max_distance'), sum('distance').as('total_distance'), 'attacker_id', 'server']) - .groupBy(['attacker_id', 'server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ kills, max_distance, total_distance, attacker_id, server }) => { - transaction = processPlayerData({ kills, max_distance, total_distance, attacker_id, server }, transaction) - }) - return transaction.exec() - }) - ) - - //server player deaths - promises.push(db.selectFrom('kill').select([count('id').as('deaths'), 'victim_id', 'server']) - .groupBy(['victim_id', 'server']).execute().then(data => { - let transaction = client.multi() - data.forEach(({ deaths, victim_id, server }) => { - transaction = transaction.HSET('servers:' + server + ':players:' + victim_id, "deaths", deaths.toString()) - }) - return transaction.exec() - })) - - await Promise.all(promises) - promises = []; - ((await client.sMembers('servers')).forEach((server: string) => { - if (isNaN(Number(server))) return - promises.push(createWeaponJson(Number(server))); - promises.push(createPlayerJson(Number(server))); - promises.push(client.sMembers('servers:' + server + ':players').then(data => { - data.forEach((player: string) => { - promises.push(createWeaponJson(Number(server), player)) - }) - })) - promises.push(client.sMembers('servers:' + server + 'weapons').then(data => { - data.forEach((weapon: string) => { - promises.push(createPlayerJson(Number(server), weapon)) - }) - })) - })); - - await Promise.all(promises) -} - -function processGlobalData({ kills, max_distance, total_distance, server }: { kills: string | number | bigint, max_distance: number, total_distance: string | number | bigint, server?: number }, transaction: any) { - const serverPrefix = server ? `servers:${server}:` : '' - const cacheLocation = serverPrefix + 'global' - transaction = transaction.HSET(cacheLocation, "total_distance", total_distance.toString()) - transaction = transaction.HSET(cacheLocation, "max_distance", max_distance.toString()) - transaction = transaction.HSET(cacheLocation, "kills", kills.toString()) - if (server) { - transaction = transaction.SADD('servers', server.toString()) - } - - return transaction -} - -function processWeaponData({ kills, max_distance, total_distance, cause_of_death, attacker_id, server }: { kills: string | number | bigint, max_distance: number, total_distance: string | number | bigint, cause_of_death: string, attacker_id?: string, server?: number }, transaction: any) { - const serverPrefix = server ? `servers:${server}:` : '' - const playerPrefix = attacker_id ? `players:${attacker_id}:` : '' - const cacheLocation = serverPrefix + playerPrefix + `weapons:` + cause_of_death - transaction = transaction.HSET(cacheLocation, "total_distance", total_distance.toString()) - transaction = transaction.HSET(cacheLocation, "max_distance", max_distance.toString()) - transaction = transaction.HSET(cacheLocation, "kills", kills.toString()) - transaction = transaction.SADD(serverPrefix + playerPrefix + `weapons`, cause_of_death.toString()) - return transaction -} - -function processPlayerData({ kills, max_distance, total_distance, attacker_id, cause_of_death, server }: { kills: string | number | bigint, max_distance: number, total_distance: string | number | bigint, attacker_id: string, server?: number, cause_of_death?: string }, transaction: any) { - const serverPrefix = server ? `servers:${server}:` : '' - const weaponPrefix = cause_of_death ? `weapons:${cause_of_death}:` : '' - const cacheLocation = serverPrefix + weaponPrefix + `players:` + attacker_id - - transaction = transaction.HSET(cacheLocation, "total_distance", total_distance.toString()) - transaction = transaction.HSET(cacheLocation, "max_distance", max_distance.toString()) - transaction = transaction.HSET(cacheLocation, "kills", kills.toString()) - transaction = transaction.SADD(serverPrefix + weaponPrefix + `players`, attacker_id.toString()) - return transaction -} - -export default processAll diff --git a/src/client/process/processPlayer.ts b/src/client/process/processPlayer.ts deleted file mode 100644 index 4e8c92c..0000000 --- a/src/client/process/processPlayer.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This file contains all functions related to population of the REDIS database for players. - */ -import cache from '../../cache/redis' -import { getPlayerReport, getPlayerSet } from '../../cache/cacheUtils' - -/** - * Combines player reports in a readable json string. - * - * ProcessPlayerReport must be called manually before this function ! - * - * Inputs `SET` `[servers:{serverId}:][weapons:{weaponId}:]players` - * - * Outputs `STRING` `[servers:{serverId}:][weapons:{weaponId}:]players:processedList` - * @param server optional server filter - * @param weapon optional player filter - * @returns - */ -export async function createPlayerJson(server?: number, weapon?: string) { - const serverPrefix = server ? `servers:${server}:` : '' - const weaponPrefix = weapon ? `weapons:${weapon}:` : '' - const cacheLocation = serverPrefix + weaponPrefix + `players` - const players = await getPlayerSet(server, weapon) - const promises: Promise[] = [] - const data: { - [playerId: string]: { - [playerData: string]: string - } - } = {} - players.forEach((plr) => { - promises.push( - (async () => { - const playerData = await getPlayerReport(plr, server, weapon) - if (Object.keys(playerData).length > 0) { - data[plr] = playerData - } - })() - ) - }) - await Promise.all(promises) - return await cache.SET(cacheLocation + `:processedList`, JSON.stringify(data)) -} diff --git a/src/client/process/processWeapon.ts b/src/client/process/processWeapon.ts deleted file mode 100644 index 2cf87d8..0000000 --- a/src/client/process/processWeapon.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This file contains all functions related to population of the REDIS database for weapons. - */ -import cache from '../../cache/redis' -import { getWeaponReport } from '../../cache/cacheUtils' - -/** - * Combines weapons reports in a readable json string. - * - * ProcessWeaponReports must be called manually before this function ! - * - * Inputs `SET` `[servers:{serverId}:][players:{playerId}:]weapons` - * - * Outputs `STRING` `[servers:{serverId}:][players:{playerId}:]weapons:processedList` - * @param server optional server filter - * @param player optional player filter - * @returns - */ -export async function createWeaponJson(server?: number, player?: string) { - const serverPrefix = server ? `servers:${server}:` : '' - const playerPrefix = player ? `players:${player}:` : '' - const cacheLocation = serverPrefix + playerPrefix + `weapons` - const weapons = await cache.SMEMBERS(cacheLocation) - const promises: Promise[] = [] - const data: { - [weaponId: string]: { - [weaponData: string]: string - } - } = {} - weapons.forEach((wpn) => { - promises.push( - (async () => { - const weaponData = await getWeaponReport(wpn, server, player) - if (Object.keys(weaponData).length > 0) { - data[wpn] = weaponData - } - })() - ) - }) - await Promise.all(promises) - return await cache.SET(cacheLocation + `:processedList`, JSON.stringify(data)) -} diff --git a/src/clientMain.ts b/src/clientMain.ts index 580d2a1..59fb734 100644 --- a/src/clientMain.ts +++ b/src/clientMain.ts @@ -4,7 +4,7 @@ import express from 'express' import cors from 'cors' import client from './client/client' import { dbReady } from './db/db' -import { cacheReady } from './cache/redis' +import processAll from './process/process' const app = express() const port = 3000 @@ -19,13 +19,11 @@ app.get('/', (req, res) => { app.use('/', client) export default - new Promise((resolve, reject) => { - dbReady().then((e) => { - cacheReady().then((e) => { - const listenServer = app.listen(port, '0.0.0.0', () => { - console.log(`Tone client api listening on port ${port}`) - resolve(listenServer) - }) - }) + new Promise(async (resolve, reject) => { + await dbReady() + await processAll() + const listenServer = app.listen(port, '0.0.0.0', () => { + console.log(`Tone client api listening on port ${port}`) + resolve(listenServer) }) - }) + }) \ No newline at end of file diff --git a/src/db/db.ts b/src/db/db.ts index c669b8f..00c72e7 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -34,77 +34,25 @@ interface RemoveFromKill { type KillRecord = Omit export async function CreateKillRecord(data: KillRecord) { - //TODO : Handle server foreign key crashes await db .insertInto('kill') .values({ ...data }) .execute() } - -export async function FindServer({ name }: { name: string }) { - return await db - .selectFrom('server') - .select(['server.name', 'server.description']) - .where('server.name', '=', name) - .executeTakeFirst() +export async function getHostList() { + return await db.selectFrom("host").select(['id', 'host.name']).execute() } - -export async function CreateServer({ - name, - description -}: { - name: string - description: string -}) { - return await db - .insertInto('server') - .values({ name, description }) - .returning(['id', 'token']) - .executeTakeFirstOrThrow() -} - //tokens are stored in raw... maybe we should use something better in the future //Using callback for express-basic-auth -export function CheckServerToken( - this: { params: any, body: any }, - name: string, - password: string, - cb: (error: Error | null, success: boolean) => void -) { - db.selectFrom('server') - .where('id', '=', Number(name)) - .where('token', '=', password) +export function CheckServerToken(token: string) { + return db.selectFrom('host').select('id') + .where('token', '=', Buffer.from(token, 'base64').toString()) .executeTakeFirst() .then((result) => { - if (name != this.params.serverId) return cb(null, false) - return cb(null, !!result) + return result }) } -/* -async function demo() { - const { id } = await db - .insertInto("person") - .values({ first_name: "Jennifer", gender: "female" }) - .returning("id") - .executeTakeFirstOrThrow(); - await db - .insertInto("pet") - .values({ name: "Catto", species: "cat", owner_id: id }) - .execute(); - - const person = await db - .selectFrom("person") - .innerJoin("pet", "pet.owner_id", "person.id") - .select(["first_name", "pet.name as pet_name"]) - .where("person.id", "=", id) - .executeTakeFirst(); - - if (person) { - person.pet_name; - } -} -*/ export async function dbReady(): Promise> { await migration return db diff --git a/src/db/migrations/2023-04-09-01 kill watcher.ts b/src/db/migrations/2023-04-09-01 kill watcher.ts new file mode 100644 index 0000000..c3438ac --- /dev/null +++ b/src/db/migrations/2023-04-09-01 kill watcher.ts @@ -0,0 +1,34 @@ +import { Kysely, sql } from 'kysely' +import { Client } from 'pg' + +const notify_newkill = ` +CREATE OR REPLACE FUNCTION notify_new_kill() + RETURNS trigger +AS +$$ +BEGIN + PERFORM pg_notify('new_kill', row_to_json(NEW)::text); + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; +` + +const pgClient = new Client({ + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DATABASE, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD +}) + +export async function up(db: Kysely): Promise { + await pgClient.connect() + await pgClient.query(notify_newkill) + await pgClient.query(`CREATE TRIGGER insert_kills_notify AFTER INSERT ON kill FOR EACH ROW EXECUTE PROCEDURE notify_new_kill();`) +} + +export async function down(db: Kysely): Promise { + await pgClient.query(`DROP FUNCTION IF EXISTS notify_new_kill;`) + await pgClient.query(`DROP TRIGGER IF EXISTS insert_kills_notify;`) + await pgClient.end() +} diff --git a/src/db/migrations/2023-04-09-02 server auth patch.ts b/src/db/migrations/2023-04-09-02 server auth patch.ts new file mode 100644 index 0000000..ff856d5 --- /dev/null +++ b/src/db/migrations/2023-04-09-02 server auth patch.ts @@ -0,0 +1,40 @@ +import { Kysely, sql } from 'kysely' +import { Client } from 'pg' + +const pgClient = new Client({ + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DATABASE, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD +}) + +export async function up(db: Kysely): Promise { + await pgClient.connect() + await pgClient.query('DROP TABLE IF EXISTS player') + + await pgClient.query('ALTER TABLE kill ADD COLUMN IF NOT EXISTS servername character varying NULL;') + await pgClient.query('ALTER TABLE kill ADD COLUMN IF NOT EXISTS host integer NULL;') + await pgClient.query('UPDATE kill SET servername = server.name FROM server WHERE server.id = kill.server;') + + await pgClient.query('CREATE TABLE host (id SERIAL PRIMARY KEY, name character varying, token character varying)') + await pgClient.query('INSERT INTO host (token) SELECT DISTINCT token FROM server') + await pgClient.query('UPDATE kill SET host = host.id FROM host FULL JOIN server ON server.token = host.token WHERE host.token = server.token AND server.id = kill.server;') + + await pgClient.query('ALTER TABLE kill ALTER COLUMN servername SET NOT NULL;') + await pgClient.query('ALTER TABLE kill DROP COLUMN server;') + await pgClient.query('DROP TABLE IF EXISTS server') + //Update the hosts column once the hoster table is manually updated + await pgClient.end() +} + +export async function down(db: Kysely): Promise { + await pgClient.connect() + await pgClient.query('ALTER TABLE kill ADD server integer NULL;') + await pgClient.query(`ALTER TABLE kill ADD server integer NULL;`) + await pgClient.query('UPDATE kill SET server = server.id FROM server where server.name = kill.servername;') + await pgClient.query('ALTER TABLE kill ALTER COLUMN server SET NOT NULL;') + await pgClient.query('ALTER TABLE kill DROP COLUMN servername;') + await pgClient.query('ALTER TABLE kill DROP COLUMN host;') + await pgClient.query('DROP TABLE IF EXISTS host') + await pgClient.end() +} diff --git a/src/db/migrations/2023-04-19-01 last aggregate.ts b/src/db/migrations/2023-04-19-01 last aggregate.ts new file mode 100644 index 0000000..d410198 --- /dev/null +++ b/src/db/migrations/2023-04-19-01 last aggregate.ts @@ -0,0 +1,46 @@ +import { Kysely, sql } from 'kysely' +import { Client } from 'pg' + +const pgClient = new Client({ + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DATABASE, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD +}) + +export async function up(db: Kysely): Promise { + await pgClient.connect() + await pgClient.query(`CREATE OR REPLACE FUNCTION public.first_agg (anyelement, anyelement) + RETURNS anyelement + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS +'SELECT $1';`) + + await pgClient.query(`CREATE AGGREGATE public.first (anyelement) ( + SFUNC = public.first_agg + , STYPE = anyelement + , PARALLEL = safe + );`) + + await pgClient.query(`CREATE OR REPLACE FUNCTION public.last_agg (anyelement, anyelement) + RETURNS anyelement + LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS +'SELECT $2';`) + + + await pgClient.query(`CREATE AGGREGATE public.last (anyelement) ( + SFUNC = public.last_agg + , STYPE = anyelement + , PARALLEL = safe + );`) + //Update the hosts column once the hoster table is manually updated + await pgClient.end() +} + +export async function down(db: Kysely): Promise { + await pgClient.connect() + pgClient.query('DROP AGGREGATE public.last') + pgClient.query('DROP FUNCTION public.last_agg') + pgClient.query('DROP AGGREGATE public.first') + pgClient.query('DROP AGGREGATE public.first_agg') + await pgClient.end() +} diff --git a/src/db/model.ts b/src/db/model.ts index d9d1fba..21e992d 100644 --- a/src/db/model.ts +++ b/src/db/model.ts @@ -2,7 +2,8 @@ import { ColumnType, Generated } from 'kysely' export interface KillTable { id: Generated - server: number + servername: string + host: number killstat_version: string match_id: string game_mode: string @@ -37,13 +38,15 @@ export interface KillTable { cause_of_death: string distance: number } - +/* interface PlayerTable { id: number name: string 'opt-out': boolean hide_TOS: boolean } +*/ + interface WeaponTable { id: string name: string @@ -56,18 +59,17 @@ interface MapTable { description: string image: string } -interface ServerTable { +interface HosterTable { id: Generated name: string - description: string token: Generated } interface Database { kill: KillTable - player: PlayerTable + //player: PlayerTable weapon: WeaponTable maps: MapTable - server: ServerTable + host: HosterTable } export default Database diff --git a/src/process/onKill.ts b/src/process/onKill.ts new file mode 100644 index 0000000..0accb23 --- /dev/null +++ b/src/process/onKill.ts @@ -0,0 +1,76 @@ +import { Client } from 'pg' +import { allData } from './process' + +const pgClient = new Client({ + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DATABASE, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD +}) + +export default async function listenKills() { + await pgClient.connect() + await pgClient.query('LISTEN new_kill') + + pgClient.on('notification', (data) => { + if (!data.payload) return + const payload = JSON.parse(data.payload) + let killEntry = allData.find( + (e) => + e.attacker_id == payload.attacker_id && + e.cause_of_death == payload.cause_of_death && + e.game_mode == payload.game_mode && + e.host == payload.host && + e.map == payload.map && + e.servername == payload.servername + ) + let deathEntry = allData.find( + (e) => + e.attacker_id == payload.victim_id && + e.cause_of_death == payload.cause_of_death && + e.game_mode == payload.game_mode && + e.host == payload.host && + e.map == payload.map && + e.servername == payload.servername + ) + if (!killEntry) { + killEntry = { + kills: 0, + deaths: 0, + max_distance: 0, + total_distance: 0, + attacker_name: payload.attacker_name, + attacker_id: payload.attacker_id, + cause_of_death: payload.cause_of_death, + game_mode: payload.game_mode, + host: payload.host, + map: payload.map, + servername: payload.servername + } + allData.push(killEntry) + } + + if (!deathEntry) { + deathEntry = { + kills: 0, + deaths: 0, + max_distance: 0, + total_distance: 0, + attacker_name: payload.victim_name, + attacker_id: payload.victim_id, + cause_of_death: payload.victim_current_weapon, + game_mode: payload.game_mode, + host: payload.host, + map: payload.map, + servername: payload.servername + } + allData.push(deathEntry) + } + + killEntry.kills++ + killEntry.total_distance += payload.distance + killEntry.max_distance = Math.max(killEntry.max_distance, payload.distance) + deathEntry.deaths++ + }) + return pgClient +} diff --git a/src/process/process.ts b/src/process/process.ts new file mode 100644 index 0000000..132926e --- /dev/null +++ b/src/process/process.ts @@ -0,0 +1,94 @@ +import db from '../db/db' +const { count, max, sum, coalesce } = db.fn +import { sql } from 'kysely' + +export let allData: Awaited> + +/** + * populates allData + * @returns + */ +async function processAll() { + console.log('Starting data calculation...') + const timeStart = new Date() + + allData = await processGlobalStats() + + console.log( + 'Data calculation finished. Took + ' + + Math.abs(new Date().getTime() - timeStart.getTime()) / 1000 + + ' seconds' + ) + if (process.env.ENVIRONMENT == 'production') { + return + } +} + +async function processGlobalStats() { + return await db + .with('kills', (db) => + db + .selectFrom('kill') + .select([ + count('id').as('kills'), + coalesce( + max('distance'), + sql`0` + ).as('max_distance'), + coalesce(sum('distance'), sql`0`).as( + 'total_distance' + ), + 'attacker_id', + sql`last(attacker_name)`.as('attacker_name'), + 'cause_of_death', + 'map', + 'game_mode', + 'servername', + 'host' + ]) + .whereRef('attacker_id', '!=', 'victim_id') + .groupBy([ + 'attacker_id', + 'cause_of_death', + 'map', + 'servername', + 'host', + 'game_mode' + ]) + ) + .with('deaths', (db) => + db + .selectFrom('kill') + .select([ + count('id').as('deaths'), + 'victim_id', + 'victim_current_weapon', + sql`last(attacker_name)`.as('attacker_name'), + 'map', + 'game_mode', + 'servername', + 'host' + ]) + .groupBy([ + 'victim_id', + 'victim_current_weapon', + 'map', + 'servername', + 'host', + 'game_mode' + ]) + ) + .selectFrom('kills') + .leftJoin('deaths', (join) => + join + .onRef('deaths.victim_id', '=', 'kills.attacker_id') + .onRef('deaths.victim_current_weapon', '=', 'kills.cause_of_death') + .onRef('deaths.servername', '=', 'kills.servername') + .onRef('deaths.host', '=', 'kills.host') + .onRef('deaths.game_mode', '=', 'kills.game_mode') + ) + .selectAll('kills') + .select(sql`COALESCE(deaths.deaths, 0)`.as('deaths')) + .execute() +} +export default processAll diff --git a/src/processMain.ts b/src/processMain.ts deleted file mode 100644 index 76ae4d7..0000000 --- a/src/processMain.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as dotenv from 'dotenv' -dotenv.config() -import { dbReady } from './db/db' -import { cacheReady } from './cache/redis' -import processAll from './client/process' - -async function startup() { - await dbReady() - await cacheReady() - await processAll() -} -export default startup() \ No newline at end of file diff --git a/src/server/register.ts b/src/server/register.ts deleted file mode 100644 index d57f994..0000000 --- a/src/server/register.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Router } from 'express' -import { GetRequest, validateErrors } from '../common' -import { FindServer, CreateServer } from '../db/db' -import { body, validationResult } from 'express-validator' - -const verificationString = 'I am a northstar server!' -const masterServerURL = 'https://northstar.tf' - -const router = Router() - -const hostsCount: { [id: string]: number } = {} -const hostsTimeout: { [id: string]: NodeJS.Timeout } = {} - -//Very simple rate limiting. max 2 registers per IP every 5 mins. Maybe 2 is a bit few ? -router.post('/register', (req, res, next) => { - let ip = - req.header('x-forwarded-for') || req.socket.remoteAddress || 'undefined' - if (hostsCount[ip] > 2) { - return res.status(429).json({ - error: - 'too many requests. Please wait 5 minutes before requesting a register again.' - }) - } - clearTimeout(hostsTimeout[ip]) - hostsCount[ip] = hostsCount[ip] ?? (hostsCount[ip] + 1) | 1 - hostsTimeout[ip] = setTimeout(() => { - hostsCount[ip] = 0 - }, 300000) - - next() -}) - -router.post( - '/register', - body(['name', 'description']).isString().withMessage('must be strings'), - body('auth_port') - .toInt() - .isInt({ min: 1, max: 65535 }) - .withMessage('must be between 1 and 65535'), - validateErrors, - async (req, res) => { - try { - //Check if server name isn't already in database - if (!!(await FindServer({ name: req.body.name }))) { - return res - .status(403) - .json({ error: 'Server already exists in the database' }) - } - - //Check if server is in masterserver's list - const masterServerList = JSON.parse( - await GetRequest(masterServerURL + '/client/servers') - ) as Array - if (!masterServerList.find((e) => e.name == req.body.name)) { - return res - .status(403) - .json({ error: 'Server not listed in masterserver' }) - } - - //Send request to verify server. Not very useful for now, but maybe a future method for auth ? - //Maybe should set a blacklist here for local domain ? - const endpoint = - 'http://' + - (req.header('x-forwarded-for') || req.socket.remoteAddress) + - ':' + - req.body.auth_port + - '/verify' - if ((await GetRequest(endpoint)) != verificationString) { - return res.status(400).json({ - error: "Couldn't reach gameserver at " + endpoint - }) - } - - //send token here - res.status(201).json( - await CreateServer({ - name: req.body.name, - description: req.body.description - }) - ) - } catch (e) { - console.log(e) - return res.status(400).json({ - error: "Server encountered an error, Couldn't register gameserver." - }) - } - } -) - -export default router diff --git a/src/server/server.ts b/src/server/server.ts index 1e5787e..3a05183 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,37 +1,31 @@ -import { NextFunction, Router } from 'express' -import expressBasicAuth from 'express-basic-auth' -import { body, header, param } from 'express-validator' -import register from './register' +import { Router } from 'express' +import { body, header } from 'express-validator' import { CreateKillRecord, CheckServerToken } from '../db/db' import { validateErrors } from '../common' const router = Router() -router.use('/', register) - //auth middleware router.post( - '/:serverId*', + '/*', header('authorization') .exists({ checkFalsy: true }) .withMessage('Missing Authorization Header') .bail() - .contains('Basic') - .withMessage('Authorization Token is not Basic'), + .custom(e => e.split(' ')[0].toLowerCase() == 'bearer') + .withMessage('Authorization Token is not Bearer'), validateErrors, - //Huge mess to retrieve server id from expressBasicAuth. We probably should fix it. - (req, res, next) => { - if (!req) res.sendStatus(500) - return expressBasicAuth({ - authorizeAsync: true, - authorizer: CheckServerToken.bind(req), - unauthorizedResponse: { error: 'invalid credentials' } - })(req as any, res, next) + async (req, res, next) => { + if (!req) return res.sendStatus(500) + if (!req.headers.authorization) return res.sendStatus(403) + const query = await CheckServerToken(req.headers.authorization.split(' ')[1]) + if (!query || !query.id) return res.sendStatus(403) + next() } ) //Route to check auth -router.post('/:serverId', (req, res) => { +router.post('/', (req, res) => { res.sendStatus(200) }) @@ -39,8 +33,8 @@ const serversCount: { [id: string]: number } = {} const serversTimeout: { [id: string]: NodeJS.Timeout } = {} //same rate limiting code as register. max 10 kills per server every 1 sec. should be enough. -router.post('/:serverId/kill', (req, res, next) => { - let serverId = Number(req.query.serverId) +/*router.post('/kill', (req, res, next) => { + let host = Number(req.query.serverId) if (serversCount[serverId] > 2) { return res.status(429).json({ error: 'too many requests. Are players really making that much kills ?' @@ -53,11 +47,10 @@ router.post('/:serverId/kill', (req, res, next) => { serversCount[serverId] = 0 }, 1000) next() -}) +})*/ router.post( - '/:serverId/kill', - param('serverId').exists().toInt().isInt(), + '/kill', body([ 'attacker_current_weapon_mods', 'attacker_weapon_1_mods', @@ -104,6 +97,7 @@ router.post( .withMessage('must be a valid float'), body( [ + 'servername', 'attacker_id', 'victim_id', 'killstat_version', @@ -131,9 +125,17 @@ router.post( min: 0 }), body(['cause_of_death', 'victim_id'], 'mandatory').exists().notEmpty(), + body('servername').customSanitizer(e => e.replace(/[^a-z0-9]/gi, '')), validateErrors, - (req, res) => { + async (req, res) => { + if (!req.headers.authorization) return res.sendStatus(403) + const headers = req.headers.authorization.split(' ') + if (headers[0].toLowerCase() != "bearer") return res.status(403).send("authorization must be token bearer") + const query = (await CheckServerToken(headers[1])) + if (!query) return res.sendStatus(403) + const host = query.id const { + servername, killstat_version, match_id, game_mode, @@ -167,13 +169,10 @@ router.post( cause_of_death, distance } = req.body - if (!req.params.serverId) { - res.status(500).send('serverId cannot be undefined') - return - } CreateKillRecord({ killstat_version, - server: Number(req.params.serverId), + servername, + host, match_id, game_mode, map, @@ -209,16 +208,15 @@ router.post( .then((e) => { res.sendStatus(201) console.log( - `[${Date.now().toLocaleString()}] Kill submitted for server ${ - req.params.serverId - }, ${attacker_name} killed ${victim_name}` + `[${Date.now().toLocaleString()}] Kill submitted for server ${servername}, ${attacker_name} killed ${victim_name}` ) }) .catch((e) => { res.sendStatus(500) console.log({ killstat_version, - server: Number(req.params.serverId), + servername, + host, match_id, game_mode, map, diff --git a/src/serverMain.ts b/src/serverMain.ts index 9d861c2..0f26a0f 100644 --- a/src/serverMain.ts +++ b/src/serverMain.ts @@ -25,7 +25,3 @@ export default new Promise((resolve, reject) => { }) }) }) - -app.on('close', () => { - -}) diff --git a/tests/client.test.ts b/tests/client.test.ts index c118a60..7a057d0 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,6 +1,7 @@ import { afterAll, beforeAll, describe, expect, test } from '@jest/globals' import clientMain from '../src/clientMain' import * as dotenv from 'dotenv' +import db from '../src/db/db' dotenv.config() let listenServer @@ -13,10 +14,10 @@ describe('client', () => { test('server list', async () => { const request = await fetch("http://127.0.0.1:3000/servers") const data = await request.json() - expect(data.length).toBeGreaterThan(0) - expect(data[0]).toHaveProperty('name') - expect(data[0]).toHaveProperty('id') - expect(data[0]).toHaveProperty('description') + const first = Object.entries(data)[0] + expect(first[1]).toHaveProperty('max_distance') + expect(first[1]).toHaveProperty('total_distance') + expect(first[1]).toHaveProperty('kills') }) test('player list', async () => { @@ -28,6 +29,15 @@ describe('client', () => { expect(first[1]).toHaveProperty('kills') }) + test('player list with weapon filter', async () => { + const request = await fetch("http://127.0.0.1:3000/players?weapons=sniper") + const data = await request.json() + const first = Object.entries(data)[0] + expect(first[1]).toHaveProperty('max_distance') + expect(first[1]).toHaveProperty('total_distance') + expect(first[1]).toHaveProperty('kills') + }) + test('weapon list', async () => { const request = await fetch("http://127.0.0.1:3000/weapons") const data = await request.json() @@ -36,10 +46,20 @@ describe('client', () => { expect(first[1]).toHaveProperty('total_distance') expect(first[1]).toHaveProperty('kills') }) + + test('weapon list with player filter', async () => { + const request = await fetch("http://127.0.0.1:3000/weapons?players=1005930844007") + const data = await request.json() + const first = Object.entries(data)[0] + expect(first[1]).toHaveProperty('max_distance') + expect(first[1]).toHaveProperty('total_distance') + expect(first[1]).toHaveProperty('kills') + }) }) afterAll((done) => { - listenServer.close(() => { + listenServer.close(async () => { + await db.destroy() done() }) }) \ No newline at end of file diff --git a/tests/realtime.test.ts b/tests/realtime.test.ts new file mode 100644 index 0000000..dacce00 --- /dev/null +++ b/tests/realtime.test.ts @@ -0,0 +1,143 @@ +import { afterAll, beforeAll, describe, expect, jest, test } from '@jest/globals' +import clientMain from '../src/clientMain' +import serverMain from '../src/serverMain' +import listenKills from "../src/process/onKill" +import * as dotenv from 'dotenv' +import db from '../src/db/db' +dotenv.config() + +let listenClient +let listenServer +let pgClient + +const data = { + attacker_weapon_1_mods: 0, + victim_id: '0', + servername: 'testServer', + victim_name: 'TestVictim', + victim_offhand_weapon_2: 0, + attacker_offhand_weapon_3: '0', + victim_weapon_3_mods: 0, + attacker_weapon_2_mods: 0, + attacker_offhand_weapon_1: 0, + attacker_weapon_3_mods: 0, + attacker_offhand_weapon_2: 0, + victim_offhand_weapon_3: '0', + victim_offhand_weapon_1: 0, + attacker_weapon_3: 'defender', + killstat_version: 'ks_3.0.0', + attacker_weapon_1: 'smr', + match_id: '31b1f34d', + distance: 0, + victim_current_weapon: 'smr', + cause_of_death: 'smr', + victim_weapon_2_mods: 0, + victim_current_weapon_mods: 0, + attacker_current_weapon_mods: 0, + game_time: 377.799, + player_count: 1, + attacker_current_weapon: 'smr', + attacker_id: '1', + game_mode: 'tdm', + map: 'thaw', + attacker_weapon_2: 'autopistol', + victim_weapon_1: 'smr', + victim_weapon_2: 'autopistol', + victim_weapon_1_mods: 0, + victim_weapon_3: 'defender', + attacker_name: 'TestAttacker' +} + +function waitFor(time: number) { + return new Promise((resolve, reject) => { + setTimeout(resolve, time) + }) +} + +jest.setTimeout(15000) + +beforeAll(async () => { + jest.setTimeout(15000) + listenClient = await clientMain; + listenServer = await serverMain; + pgClient = await listenKills() + const yea = waitFor(1000) + const response = await fetch(`http://127.0.0.1:3001/kill`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + }, + body: JSON.stringify(data), // body data type must match "Content-Type" header + }); + expect(response.status).toBe(201) + await yea +}) + +describe('realtime', () => { + let playerKills + let weaponKills + let playerDeaths + test('fetch player', async () => { + const request = await fetch("http://127.0.0.1:3000/players") + const data = await request.json() + const player = data["1"] + expect(player).toHaveProperty('max_distance') + expect(player).toHaveProperty('total_distance') + expect(player).toHaveProperty('kills') + playerKills = player.kills + playerDeaths = player.deaths + }) + + test('fetch weapon', async () => { + const request = await fetch("http://127.0.0.1:3000/weapons") + const data = await request.json() + const weapon = data.smr + expect(weapon).toHaveProperty('max_distance') + expect(weapon).toHaveProperty('total_distance') + expect(weapon).toHaveProperty('kills') + weaponKills = weapon.kills + }) + + test('update player', async () => { + jest.setTimeout(15000) + const yea = waitFor(1000) + const response = await fetch(`http://127.0.0.1:3001/kill`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + }, + body: JSON.stringify(data), // body data type must match "Content-Type" header + }); + expect(response.status).toBe(201) + await yea + }) + + test('check player update', async () => { + const request = await fetch("http://127.0.0.1:3000/players") + const data = await request.json() + const player = data["1"] + expect(player.kills).toBe(playerKills + 1) + }) + + test('check weapon update', async () => { + const request = await fetch("http://127.0.0.1:3000/weapons") + const data = await request.json() + const weapon = data.smr + expect(weapon.kills).toBe(weaponKills + 1) + }) + +}) + +afterAll((done) => { + listenClient.close(() => { + listenServer.close(async () => { + await pgClient.end() + await db.destroy() + done() + }) + }) +}) \ No newline at end of file diff --git a/tests/server.test.ts b/tests/server.test.ts index b5cbba3..75f4d18 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -11,13 +11,49 @@ beforeAll(async () => { }) describe('server', () => { - test('server auth prefetch', async () => { - const response = await fetch(`http://127.0.0.1:3001/${process.env.SERVERAUTH_ID}`, { + + test('bad auth prefetch', async () => { + const response = await fetch(`http://127.0.0.1:3001/`, { method: "POST", // *GET, POST, PUT, DELETE, etc. credentials: "same-origin", // include, *same-origin, omit headers: { "Content-Type": "application/json", - 'Authorization': `Basic ${Buffer.from(process.env.SERVERAUTH_ID + ':' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + 'Authorization': `Bearere ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + } + }); + expect(response.status).toBe(400) + + const response2 = await fetch(`http://127.0.0.1:3001/`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${Buffer.from('badtoken').toString('base64')}` + } + }); + expect(response2.status).toBe(403) + }) + + test('bad kill token', async () => { + const response2 = await fetch(`http://127.0.0.1:3001/kill`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${Buffer.from('badtoken').toString('base64')}` + } + }); + expect(response2.status).toBe(403) + }) + + + test('good auth prefetch', async () => { + const response = await fetch(`http://127.0.0.1:3001/`, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + 'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}` } }); expect(response.status).toBe(200) @@ -25,6 +61,7 @@ describe('server', () => { test('register a kill', async () => { const data = { + servername: 'testserver', attacker_weapon_1_mods: 0, victim_id: '0', victim_name: 'TestVictim', @@ -60,21 +97,23 @@ describe('server', () => { victim_weapon_3: 'defender', attacker_name: 'TestAttacker' } - const response = await fetch(`http://127.0.0.1:3001/${process.env.SERVERAUTH_ID}/kill`, { + const response = await fetch(`http://127.0.0.1:3001/kill`, { method: "POST", // *GET, POST, PUT, DELETE, etc. credentials: "same-origin", // include, *same-origin, omit headers: { "Content-Type": "application/json", - 'Authorization': `Basic ${Buffer.from(process.env.SERVERAUTH_ID + ':' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + 'Authorization': `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}` }, body: JSON.stringify(data), // body data type must match "Content-Type" header }); + console.log(await response.text()) expect(response.status).toBe(201) }) test('register a kill with missing data', async () => { const data = { + servername: 'testserver', attacker_weapon_1_mods: NaN, victim_id: '0', victim_name: 'TestVictim', @@ -110,12 +149,12 @@ describe('server', () => { victim_weapon_3: 'arc_launcher', attacker_name: 'TestAttacker' } - const response = await fetch(`http://127.0.0.1:3001/${process.env.SERVERAUTH_ID}/kill`, { + const response = await fetch(`http://127.0.0.1:3001/kill`, { method: "POST", // *GET, POST, PUT, DELETE, etc. credentials: "same-origin", // include, *same-origin, omit headers: { "Content-Type": "application/json", - 'Authorization': `Basic ${Buffer.from(process.env.SERVERAUTH_ID + ':' + process.env.SERVERAUTH_TOKEN).toString('base64')}` + 'Authorization': `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}` }, body: JSON.stringify(data), // body data type must match "Content-Type" header }); @@ -124,9 +163,10 @@ describe('server', () => { }) afterAll((done) => { - listenServer.close(() => { - db.deleteFrom('kill').where('attacker_id', '=', '0').orWhere('attacker_id', '=', '1').execute().then(() => { - db.destroy().then(() => { done() }) + db.deleteFrom('kill').where('attacker_id', '=', '0').orWhere('attacker_id', '=', '1').execute().then(() => { + listenServer.close(async () => { + await db.destroy() + done() }) }) -}) \ No newline at end of file +})