diff --git a/backend/assets/html.details.pages.mock.js b/backend/assets/html.details.pages.mock.js index b3b77e1be..b789b3f9d 100644 --- a/backend/assets/html.details.pages.mock.js +++ b/backend/assets/html.details.pages.mock.js @@ -1,5 +1,21 @@ +import { parseHTML } from "linkedom"; + +export function createHtmlDetailsPage(page) { + return { page, id: 1 }; +} + export function createHtmlDetailsPages(pages) { return pages.map((page, index) => { return { page, id: index + 1 }; }); } + +export function getParsedHtmlPage(page) { + return parseHTML(page).document; +} + +export function getParsedHtmlPages(pages) { + return pages.map((page, index) => { + return { page: parseHTML(page).document, id: index + 1 }; + }); +} diff --git a/backend/assets/steam-details-pages/feartress.game.html.details.page.js b/backend/assets/steam-details-pages/feartress.game.html.details.page.js index be6cac23a..e9bca5bd9 100644 --- a/backend/assets/steam-details-pages/feartress.game.html.details.page.js +++ b/backend/assets/steam-details-pages/feartress.game.html.details.page.js @@ -821,7 +821,7 @@ export const feartressGameHtmlDetailsPage = `
Release
- TBD
+ 10.10.2001 @@ -842,7 +842,7 @@ export const feartressGameHtmlDetailsPage = `
Release Date:
-
TBD
+
1 January, 2001
diff --git a/backend/assets/steamdb-details-pages/elden.ring.html.details.page.js b/backend/assets/steamdb-details-pages/elden.ring.html.details.page.js new file mode 100644 index 000000000..82ceb4486 --- /dev/null +++ b/backend/assets/steamdb-details-pages/elden.ring.html.details.page.js @@ -0,0 +1,345 @@ +export const eldenRingHtmlDetailsPageDb = ` + + + + + + +ELDEN RING · SteamDB + + + + + + + + + + + + + + + + + + + + + + +Skip to content + +
+
+

Start typing to see game suggestions. This only suggests apps that have a store page.

+
    +
  • Use slash key (/) to focus search from anywhere.
  • +
  • Use arrow keys ( and ) to navigate suggestions.
  • +
  • Use escape (Esc) to close search.
  • +
  • Enter an appid to be redirected to the app page.
  • +
  • Enter a steamid (765...) to be redirected to calculator.
  • +
  • Paste a profile link (/id/ or /profiles/) to be redirected to calculator.
  • +
  • Paste any url that contains app/, sub/, bundle/, or depot/ to be redirected to the relevant page.
  • +
  • Search is powered by Algolia.
  • +
+
+
+
+Only apps with a store page are suggested. + +Enter Submit to view all results. +Ctrl+Enter View and filter in instant search. + +
+
+
Close ×
+
+
+ + + + + + + +
+ + +
+ + + + + + +
+ + +
+
+
+
+ +

ELDEN RING

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
App ID1245620
App TypeGame
Developer +
PublisherFromSoftware Inc., Bandai Namco Entertainment +
FranchiseBandai Namco Entertainment +
Supported Systems + Windows + + +
Technologies AntiCheat.EasyAntiCheat, SDK.EpicOnlineServices +
Last Change Number 22758623
Last Record Update14 March 2024 – 09:04:36 UTC ()
Release Date24 February 2022 – 23:07:00 UTC ()
+ +
+
+
+ +
+ + +

THE NEW FANTASY ACTION RPG. Rise, Tarnished, and be guided by grace to brandish the power of the Elden Ring and become an Elden Lord in the Lands Between.

+
+
+
+
+ +` \ No newline at end of file diff --git a/backend/src/adapters/driven/http/steam.client.js b/backend/src/adapters/driven/http/steam.client.js index cedacef66..f04dd2b33 100644 --- a/backend/src/adapters/driven/http/steam.client.js +++ b/backend/src/adapters/driven/http/steam.client.js @@ -1,4 +1,5 @@ import { SteamApp } from "../../../core/models/steam.app.js"; +import { ValidDataSources } from "../../../core/models/valid.data.sources.js"; export class SteamClient { #httpClient; @@ -33,38 +34,27 @@ export class SteamClient { ).map((player) => (player ? player.data.response.player_count : 0)); } - async getSteamAppHtmlDetailsPage(id) { - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 - // We add the try catch block here to not crash the whole backend when our application comes accross - // errors written into the Steam API - try { - const url = `https://store.steampowered.com/app/${id}`; - - return (await this.#httpClient.get(url)).data; - } catch (err) { - return ""; - } - } + // TODO https://github.com/lukatarman/steam-game-stats/issues/192 + async getSourceHtmlDetailsPage(id, source) { + const url = this.getSourceUrl(id, source); - async getSteamchartsGameHtmlDetailsPage(id) { - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 try { - const url = `https://steamcharts.com/app/${id}`; - return (await this.#httpClient.get(url)).data; } catch (err) { return ""; } } - async getSteamDbHtmlDetailsPage(id) { - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 - try { - const url = `https://steamdb.info/app/${id}/info/`; + getSourceUrl(id, source) { + switch (source) { + case ValidDataSources.validDataSources.steamWeb: + return `https://store.steampowered.com/app/${id}`; - return (await this.#httpClient.get(url)).data; - } catch (error) { - return ""; + case ValidDataSources.validDataSources.steacharts: + return `https://steamcharts.com/app/${id}`; + + case ValidDataSources.validDataSources.steamDb: + return `https://steamdb.info/app/${id}/info/`; } } } diff --git a/backend/src/core/features/game-identifier/game.identifier.js b/backend/src/core/features/game-identifier/game.identifier.js index 571c93574..d778c5273 100644 --- a/backend/src/core/features/game-identifier/game.identifier.js +++ b/backend/src/core/features/game-identifier/game.identifier.js @@ -1,14 +1,6 @@ -import { - discoverGamesFromSteamWeb, - updateTypeSideEffectFree, - identifyGames, - assignType, - updateMissingDetails, - updateMissingReleaseDates, - recordAttemptsViaSteamDb, -} from "../../services/game.service.js"; import { delay } from "../../../common/time.utils.js"; import { HistoryCheck } from "../../models/history.check.js"; +import { ValidDataSources } from "../../models/valid.data.sources.js"; export class GameIdentifier { #steamClient; @@ -17,6 +9,7 @@ export class GameIdentifier { #historyChecksRepository; #logger; #options; + #htmlParser; constructor( steamClient, @@ -25,6 +18,7 @@ export class GameIdentifier { historyChecksRepository, logger, options, + htmlParser, ) { this.#steamClient = steamClient; this.#steamAppsRepository = steamAppsRepository; @@ -32,196 +26,147 @@ export class GameIdentifier { this.#historyChecksRepository = historyChecksRepository; this.#logger = logger; this.#options = options; + this.#htmlParser = htmlParser; } - tryViaSteamWeb = async () => { - this.#logger.debugc("identifying games via steam web"); + checkIfGameViaSource = async (source) => { + this.#logger.debugc(`identifying games via ${source}`); - const steamApps = await this.#steamAppsRepository.getSteamWebUntriedFilteredSteamApps( + const steamApps = await this.#steamAppsRepository.getSourceUntriedFilteredSteamApps( this.#options.batchSize, + source, ); - if (steamApps.length === 0) { + + if (steamApps.isEmpty) { this.#logger.debugc( `no steam apps in db, retry in: ${this.#options.globalIterationDelay} ms`, ); + return; } - const [games, updatedSteamApps] = await this.#identifyViaSteamWeb(steamApps); - - this.#persist(games, updatedSteamApps); - }; + const htmlDetailsPages = await this.#getSteamAppsHtmlDetailsPages( + steamApps.content, + source, + ); - async #identifyViaSteamWeb(steamApps) { - const htmlDetailsPages = await this.#getSteamAppsHtmlDetailsPages(steamApps); + steamApps.identifyTypes(htmlDetailsPages, source); - const games = discoverGamesFromSteamWeb(steamApps, htmlDetailsPages); + const games = steamApps.extractGames(htmlDetailsPages, source); - const updatedSteamApps = updateTypeSideEffectFree(steamApps, htmlDetailsPages); + await this.#persistGameCheckUpdates(games, steamApps.content); + }; - return [games, updatedSteamApps]; - } + async #getSteamAppsHtmlDetailsPages(steamApps, source) { + const htmlDetailsPages = []; - async #getSteamAppsHtmlDetailsPages(steamApps) { - const detailsPages = []; for (let steamApp of steamApps) { - detailsPages.push( - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 - await this.#steamClient.getSteamAppHtmlDetailsPage(steamApp.appid), + // TODO https://github.com/lukatarman/steam-game-stats/issues/192 + const htmlPage = await this.#steamClient.getSourceHtmlDetailsPage( + steamApp.appid, + source, ); + + htmlDetailsPages.push({ + page: this.#htmlParser(htmlPage).document, + id: steamApp.appid, + }); + await delay(this.#options.unitDelay); } - return detailsPages; + + return htmlDetailsPages; } - async #persist(games, updatedSteamApps) { + async #persistGameCheckUpdates(games, steamApps) { if (games.length !== 0) { this.#logger.debugc(`persiting ${games.length} identified games`); + await this.#gamesRepository.insertManyGames(games); await this.#historyChecksRepository.insertManyHistoryChecks( HistoryCheck.manyFromGames(games), ); } - await this.#steamAppsRepository.updateSteamAppsById(updatedSteamApps); - } - - tryViaSteamchartsWeb = async () => { - this.#logger.debugc("identifying games via steamcharts web"); - - const steamApps = - await this.#steamAppsRepository.getSteamchartsUntriedFilteredSteamApps( - this.#options.batchSize, - ); - if (steamApps.length === 0) { - this.#logger.debugc( - `no steam apps in db, retry in: ${this.#options.globalIterationDelay} ms`, - ); - return; - } - const updatedSteamApps = await this.#updateStatusViaSteamchartsWeb(steamApps); - const games = identifyGames(updatedSteamApps); - - this.#persist(games, updatedSteamApps); - }; - - // this method is a mess right now, including the tests. Will be fixed in issue: #198 - async #updateStatusViaSteamchartsWeb(steamApps) { - const updatedSteamApps = []; - - for (let steamApp of steamApps) { - const steamAppCopy = steamApp.copy(); - steamAppCopy.triedViaSteamchartsWeb(); - - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 - const result = await this.#steamClient.getSteamchartsGameHtmlDetailsPage( - steamApp.appid, - ); - - if (result === "") { - steamAppCopy.failedViaSteamchartsWeb(); - this.#logger.debugc(`no entry on steamcharts web for appid: ${steamApp.appid}`); - } - - assignType(result, steamAppCopy); - - await delay(this.#options.unitDelay); - updatedSteamApps.push(steamAppCopy); - } - - return updatedSteamApps; + await this.#steamAppsRepository.updateSteamAppsById(steamApps); } updateGamesWithoutDetails = async () => { + const source = ValidDataSources.validDataSources.steamDb; this.#logger.debugc("updating games without details"); const games = await this.#gamesRepository.getGamesWithoutDetails( this.#options.batchSize, ); - if (games.length === 0) { + if (games.isEmpty) { this.#logger.debugc( - `no games without details in db, retrying in ${ + `no games without details in db, retry in: ${ this.#options.globalIterationDelay } ms`, ); return; } - const steamApps = await this.#steamAppsRepository.getSteamAppsById( - games.map((game) => game.id), - ); + const steamApps = await this.#steamAppsRepository.getSteamAppsById(games.ids); - const htmlDetailsPages = await this.#getSteamDbHtmlDetailsPages(games); + const htmlDetailsPages = await this.#getSteamAppsHtmlDetailsPages( + steamApps.content, + source, + ); - const updatedApps = recordAttemptsViaSteamDb(steamApps, htmlDetailsPages); + steamApps.recordAttemptsViaSource(htmlDetailsPages, source); - updateMissingDetails(games, htmlDetailsPages); + games.updateGameDetailsFrom(htmlDetailsPages); - this.#persistMissingProperties(games, updatedApps); + this.#persistUpdatedDetails(games.content, steamApps.content); }; - async #getSteamDbHtmlDetailsPages(games) { - const htmlDetailsPages = []; - - for (let game of games) { - // TODO https://github.com/lukatarman/steam-game-stats/issues/192 - const htmlPage = await this.#steamClient.getSteamDbHtmlDetailsPage(game.id); - htmlDetailsPages.push({ page: htmlPage, id: game.id }); - - await delay(this.#options.unitDelay); - } - - return htmlDetailsPages; - } - - async #persistMissingProperties(games, appsWithoutPages) { - if (appsWithoutPages.length !== 0) { - this.#logger.debugc(`persisting ${appsWithoutPages.length} apps without pages`); - this.#steamAppsRepository.updateSteamAppsById(appsWithoutPages); - } - + async #persistUpdatedDetails(games, steamApps) { + this.#logger.debugc(`persisting ${steamApps.length} apps with updated html attempts`); this.#logger.debugc(`persisting ${games.length} games with updated details`); - await this.#gamesRepository.updateGameDetails(games); + + await this.#steamAppsRepository.updateSteamAppsById(steamApps); + await this.#gamesRepository.updateGameDetailsFrom(games); } updateGamesWithoutReleaseDates = async () => { - this.#logger.debugc("updating games without details"); + const source = ValidDataSources.validDataSources.steamDb; + this.#logger.debugc(`updating games without release dates via ${source}`); const games = await this.#gamesRepository.getGamesWithoutReleaseDates( this.#options.batchSize, ); - if (games.length === 0) { + if (games.isEmpty) { this.#logger.debugc( - `no games without release dates in db, retrying in ${ - this.#options.iterationDelay + `no games without release dates in db, retry in: ${ + this.#options.globalIterationDelay } ms`, ); + return; } - const steamApps = await this.#steamAppsRepository.getSteamAppsById( - games.map((game) => game.id), - ); + const steamApps = await this.#steamAppsRepository.getSteamAppsById(games.ids); - const htmlDetailsPages = await this.#getSteamDbHtmlDetailsPages(games); + const htmlDetailsPages = await this.#getSteamAppsHtmlDetailsPages( + steamApps.content, + source, + ); - const updatedApps = recordAttemptsViaSteamDb(steamApps, htmlDetailsPages); + steamApps.recordAttemptsViaSource(htmlDetailsPages, source); - updateMissingReleaseDates(games, htmlDetailsPages); + games.extractReleaseDatesFrom(htmlDetailsPages); - this.#persistReleaseDates(games, updatedApps); + this.#persistReleaseDates(games.content, steamApps.content); }; - #persistReleaseDates = async (games, appsWithoutPages) => { - if (appsWithoutPages.length !== 0) { - this.#logger.debugc(`persisting ${appsWithoutPages.length} apps without pages`); - this.#steamAppsRepository.updateSteamAppsById(appsWithoutPages); - } - + async #persistReleaseDates(games, steamApps) { + this.#logger.debugc(`persisting ${steamApps.length} apps with updated html attempts`); this.#logger.debugc(`persisting ${games.length} games with updated release dates`); + await this.#steamAppsRepository.updateSteamAppsById(steamApps); await this.#gamesRepository.updateReleaseDates(games); - }; + } } diff --git a/backend/src/core/features/game-identifier/game.identifier.spec.js b/backend/src/core/features/game-identifier/game.identifier.spec.js index 440543149..682501cd3 100644 --- a/backend/src/core/features/game-identifier/game.identifier.spec.js +++ b/backend/src/core/features/game-identifier/game.identifier.spec.js @@ -1,15 +1,4 @@ import { GameIdentifier } from "./game.identifier.js"; -import { animaddicts2gameHtmlDetailsPage } from "../../../../assets/steam-details-pages/animaddicts.2.game.html.details.page.js"; -import { glitchhikersSoundtrackHtmlDetailsPage } from "../../../../assets/steam-details-pages/glitchhikers.soundtrack.html.details.page.js"; -import { eldenRingHttpDetailsSteamcharts } from "../../../../assets/steamcharts-details-pages/elden.ring.multiple.histories.html.details.page.js"; -import { SteamApp } from "../../models/steam.app.js"; -import { - discoverGamesFromSteamWeb, - identifyGames, - recordAttemptsViaSteamDb, - updateMissingReleaseDates, - updateTypeSideEffectFree, -} from "../../services/game.service.js"; import { HistoryCheck } from "../../models/history.check.js"; import { createLoggerMock } from "../../../common/logger.mock.js"; import { counterStrikeHtmlDetailsSteamDb } from "../../../../assets/steamdb-details-pages/counter.strike.html.details.page.js"; @@ -17,699 +6,494 @@ import { riskOfRainHtmlDetailsSteamDb } from "../../../../assets/steamdb-details import { getXGamesWithoutDetails } from "../../models/game.mocks.js"; import { createConfigMock } from "../../../common/config.loader.mock.js"; import { getXSampleSteamApps } from "../../models/steam.app.mocks.js"; -import { createHtmlDetailsPages } from "../../../../assets/html.details.pages.mock.js"; +import { ValidDataSources } from "../../models/valid.data.sources.js"; +import { gta5ageRestrictedHtmlDetailsPage } from "../../../../assets/steam-details-pages/gta.5.age.restricted.html.details.page.js"; +import { theSims4dlcHtmlDetailsPage } from "../../../../assets/steam-details-pages/the.sims.4.dlc.html.details.page.js"; +import { getParsedHtmlPages } from "../../../../assets/html.details.pages.mock.js"; +import { mortalDarknessGameHtmlDetailsPage } from "../../../../assets/steam-details-pages/mortal.darkness.game.html.details.page.js"; +import { SteamAppsAggregate } from "../../models/steam.apps.aggregate.js"; +import { GamesAggregate } from "../../models/games.aggregate.js"; +import { parseHTML } from "linkedom"; describe("game.identifier.js", function () { - describe(".tryViaSteamWeb", function () { - describe("gets zero steamApps from the database and stops. So,", function () { - beforeAll(async function () { - this.steamClientMock = createSteamMock([undefined]); - - this.steamAppsRepository = createSteamAppsRepositoryMock([], undefined); - this.gamesRepository = createGamesRepositoryMock(); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); + describe(".checkIfGameViaSource.", function () { + describe("via SteamWeb", function () { + describe("Finds no unidentified steam apps in the database", function () { + beforeAll(async function () { + this.source = ValidDataSources.validDataSources.steamWeb; + this.steamClient = createSteamMock([]); + this.steamAppsRepository = createSteamAppsRepositoryMock( + [], + new SteamAppsAggregate([]), + ); + this.gamesRepository = createGamesRepositoryMock([]); + this.historyChecksRepository = createHistoryChecksRepositoryMock(); - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock().features, - ); + this.identifier = new GameIdentifier( + this.steamClient, + this.steamAppsRepository, + this.gamesRepository, + this.historyChecksRepository, + createLoggerMock(), + createConfigMock().features, + parseHTML, + ); - await this.identifier.tryViaSteamWeb(); - }); + await this.identifier.checkIfGameViaSource(this.source); + }); - it("getSteamWebUntriedFilteredSteamApps was called once", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledTimes(1); - }); + it("getSourceUntriedFilteredSteamApps was called once", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledTimes(1); + }); - it("getSteamWebUntriedFilteredSteamApps was called with the correct batch size", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); - }); + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); + }); - it("getSteamAppHtmlDetailsPage was not called", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledTimes(0); - }); + it("getSourceHtmlDetailsPage was not called", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(0); + }); - it("insertManyGames was not called", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); - }); + it("insertManyGames was not called", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); + }); - it("insertManyHistoryChecks was not called", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(0); - }); + it("insertManyHistoryChecks was not called", function () { + expect( + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledTimes(0); + }); - it("updateSteamAppsById was not called", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(0); + it("updateSteamAppsById was not called", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(0); + }); }); - }); - describe("gets one game out of a batch of one steamApp, and inserts it into the database. So,", function () { - beforeAll(async function () { - this.app = getXSampleSteamApps(1); + describe("Finds two unidentified steam apps in the database, none of them being games", function () { + beforeAll(async function () { + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - this.games = discoverGamesFromSteamWeb(this.app, [ - animaddicts2gameHtmlDetailsPage, - ]); + this.source = ValidDataSources.validDataSources.steamWeb; - this.historychecks = HistoryCheck.manyFromGames(this.games); + const htmlDetailsPages = [ + gta5ageRestrictedHtmlDetailsPage, + theSims4dlcHtmlDetailsPage, + ]; - this.instantiatedApp = SteamApp.manyFromSteamApi(this.app); + const parsedHtmlPages = getParsedHtmlPages(htmlDetailsPages); - this.updatedSteamApps = updateTypeSideEffectFree(this.instantiatedApp, [ - animaddicts2gameHtmlDetailsPage, - ]); + this.steamApps.identifyTypes(parsedHtmlPages, this.source); - this.steamClientMock = createSteamMock([animaddicts2gameHtmlDetailsPage]); + this.games = this.steamApps.extractGames(parsedHtmlPages, this.source); - this.steamAppsRepository = createSteamAppsRepositoryMock( - this.instantiatedApp, - undefined, - ); - this.gamesRepository = createGamesRepositoryMock(); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); + this.historyChecks = HistoryCheck.manyFromGames(this.games); - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock().features, - ); + this.steamClient = createSteamMock(htmlDetailsPages); + this.steamAppsRepository = createSteamAppsRepositoryMock( + undefined, + this.steamApps, + ); + this.gamesRepository = createGamesRepositoryMock(); + this.historyChecksRepository = createHistoryChecksRepositoryMock(); - await this.identifier.tryViaSteamWeb(); - }); + this.identifier = new GameIdentifier( + this.steamClient, + this.steamAppsRepository, + this.gamesRepository, + this.historyChecksRepository, + createLoggerMock(), + createConfigMock().features, + parseHTML, + ); - it("getSteamWebUntriedFilteredSteamApps was called once", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledTimes(1); - }); + await this.identifier.checkIfGameViaSource(this.source); + }); - it("getSteamWebUntriedFilteredSteamApps was called with the correct batch size", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); - }); + it("getSourceUntriedFilteredSteamApps was called once", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledTimes(1); + }); - it("getSteamWebUntriedFilteredSteamApps was called before getSteamAppHtmlDetailsPage", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledBefore(this.steamClientMock.getSteamAppHtmlDetailsPage); - }); + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); + }); - it("getSteamAppHtmlDetailsPage was called once", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledTimes(1); - }); + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); + }); - it("getSteamAppHtmlDetailsPage was called with the correct id", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApp[0].appid, - ); - }); + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[0].appid, + this.source, + ); + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[1].appid, + this.source, + ); + }); - it("getSteamAppHtmlDetailsPage was called before insertManyGames", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledBefore( - this.gamesRepository.insertManyGames, - ); - }); + it("insertManyGames was not called", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); + }); - it("insertManyGames was called once", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); - }); + it("insertManyHistoryChecks was not called", function () { + expect( + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledTimes(0); + }); - it("insertManyGames was called with the correct games", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); - }); + it("updateSteamAppsById was called once", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); + }); - it("insertManyGames was called before insertManyHistoryChecks", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledBefore( - this.historyChecksRepository.insertManyHistoryChecks, - ); + it("updateSteamAppsById was called with the correct argument", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( + this.steamApps.content, + ); + }); }); - it("insertManyHistoryChecks was called once", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(1); - }); + describe("Finds two unidentified steam apps in the database, one of them being a game", function () { + beforeAll(async function () { + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - it("insertManyHistoryChecks was called with the correct history checks", function () { - expect(this.historyChecksRepository.insertManyHistoryChecks).toHaveBeenCalledWith( - this.historychecks, - ); - }); + this.source = ValidDataSources.validDataSources.steamWeb; - it("insertManyHistoryChecks was called before updateSteamAppsById", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledBefore(this.steamAppsRepository.updateSteamAppsById); - }); + const htmlDetailsPages = [ + mortalDarknessGameHtmlDetailsPage, + gta5ageRestrictedHtmlDetailsPage, + ]; - it("updateSteamAppsById was called once", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); - }); + const parsedHtmlPages = getParsedHtmlPages(htmlDetailsPages); - it("updateSteamAppsById was called with the correct steam apps", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedSteamApps, - ); - }); - }); + this.steamApps.identifyTypes(parsedHtmlPages, this.source); - describe("gets one game out of a batch of two steamApps, and inserts it into the database. So,", function () { - beforeAll(async function () { - this.apps = getXSampleSteamApps(2); + this.games = this.steamApps.extractGames(parsedHtmlPages, this.source); - this.htmlDetailsPages = [ - animaddicts2gameHtmlDetailsPage, - glitchhikersSoundtrackHtmlDetailsPage, - ]; + this.historyChecks = HistoryCheck.manyFromGames(this.games); - this.games = discoverGamesFromSteamWeb(this.apps, this.htmlDetailsPages); + this.steamClient = createSteamMock(htmlDetailsPages); + this.steamAppsRepository = createSteamAppsRepositoryMock( + undefined, + this.steamApps, + ); + this.gamesRepository = createGamesRepositoryMock(); + this.historyChecksRepository = createHistoryChecksRepositoryMock(); - this.historychecks = HistoryCheck.manyFromGames(this.games); + this.identifier = new GameIdentifier( + this.steamClient, + this.steamAppsRepository, + this.gamesRepository, + this.historyChecksRepository, + createLoggerMock(), + createConfigMock().features, + parseHTML, + ); - this.instantiatedApps = SteamApp.manyFromSteamApi(this.apps); + await this.identifier.checkIfGameViaSource(this.source); + }); - this.updatedSteamApps = updateTypeSideEffectFree( - this.instantiatedApps, - this.htmlDetailsPages, - ); + it("getSourceUntriedFilteredSteamApps was called once", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledTimes(1); + }); - this.steamClientMock = createSteamMock([ - animaddicts2gameHtmlDetailsPage, - glitchhikersSoundtrackHtmlDetailsPage, - ]); + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); + }); - this.steamAppsRepository = createSteamAppsRepositoryMock( - this.instantiatedApps, - undefined, - ); - this.gamesRepository = createGamesRepositoryMock(); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); + }); - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock().features, - ); + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[0].appid, + this.source, + ); + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[1].appid, + this.source, + ); + }); - await this.identifier.tryViaSteamWeb(); - }); + it("insertManyGames was called once", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); + }); - it("getSteamWebUntriedFilteredSteamApps was called once", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledTimes(1); - }); + it("insertManyGames was called with the correct argument", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); + }); - it("getSteamWebUntriedFilteredSteamApps was called with the correct batch size", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); - }); + it("insertManyHistoryChecks was called once", function () { + expect( + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledTimes(1); + }); - it("getSteamWebUntriedFilteredSteamApps was called before getSteamAppHtmlDetailsPage", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledBefore(this.steamClientMock.getSteamAppHtmlDetailsPage); - }); + it("insertManyHistoryChecks was called with the correct argument", function () { + expect( + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledWith(this.historyChecks); + }); - it("getSteamAppHtmlDetailsPage was called twice", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledTimes(2); - }); + it("updateSteamAppsById was called once", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); + }); - it("getSteamAppHtmlDetailsPage was called with the correct ids", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[0].appid, - ); - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[1].appid, - ); + it("updateSteamAppsById was called with the correct argument", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( + this.steamApps.content, + ); + }); }); + }); - it("getSteamAppHtmlDetailsPage was called before insertManyGames", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledBefore( - this.gamesRepository.insertManyGames, - ); - }); + describe("via Steamcharts", function () { + describe("Finds no unidentified steam apps in the database", function () { + beforeAll(async function () { + this.source = ValidDataSources.validDataSources.steamcharts; + this.steamClient = createSteamMock([]); + this.steamAppsRepository = createSteamAppsRepositoryMock( + [], + new SteamAppsAggregate([]), + ); + this.gamesRepository = createGamesRepositoryMock([]); + this.historyChecksRepository = createHistoryChecksRepositoryMock(); - it("insertManyGames was called once", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); - }); + this.identifier = new GameIdentifier( + this.steamClient, + this.steamAppsRepository, + this.gamesRepository, + this.historyChecksRepository, + createLoggerMock(), + createConfigMock().features, + parseHTML, + ); - it("insertManyGames was called with the correct games", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); - }); + await this.identifier.checkIfGameViaSource(this.source); + }); - it("insertManyGames was called before insertManyHistoryChecks", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledBefore( - this.historyChecksRepository.insertManyHistoryChecks, - ); - }); + it("getSourceUntriedFilteredSteamApps was called once", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledTimes(1); + }); - it("insertManyHistoryChecks was called once", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(1); - }); + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { + expect( + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); + }); - it("insertManyHistoryChecks was called the correct history checks", function () { - expect(this.historyChecksRepository.insertManyHistoryChecks).toHaveBeenCalledWith( - this.historychecks, - ); - }); + it("getSourceHtmlDetailsPage was not called", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(0); + }); - it("insertManyHistoryChecks was called before updateSteamAppsById", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledBefore(this.steamAppsRepository.updateSteamAppsById); - }); + it("insertManyGames was not called", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); + }); - it("updateSteamAppsById was called once", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); - }); + it("insertManyHistoryChecks was not called", function () { + expect( + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledTimes(0); + }); - it("updateSteamAppsById was called with the correct steam apps", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedSteamApps, - ); + it("updateSteamAppsById was not called", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(0); + }); }); - }); - describe("gets one game out of a batch of four steamApps.", function () { - describe("The second and third games' pages experience errors.", function () { + describe("Finds two unidentified steam apps in the database, none of them being games", function () { beforeAll(async function () { - this.apps = getXSampleSteamApps(4); + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - this.htmlDetailsPages = [ - animaddicts2gameHtmlDetailsPage, - "", - "", - glitchhikersSoundtrackHtmlDetailsPage, - ]; + this.source = ValidDataSources.validDataSources.steamcharts; - this.instantiatedApps = SteamApp.manyFromSteamApi(this.apps); + const htmlDetailsPages = ["", ""]; - this.games = discoverGamesFromSteamWeb( - this.instantiatedApps, - this.htmlDetailsPages, - ); + const parsedHtmlPages = getParsedHtmlPages(htmlDetailsPages); - this.historychecks = HistoryCheck.manyFromGames(this.games); + this.steamApps.identifyTypes(parsedHtmlPages, this.source); - this.updatedSteamApps = updateTypeSideEffectFree( - this.instantiatedApps, - this.htmlDetailsPages, - ); + this.games = this.steamApps.extractGames(parsedHtmlPages, this.source); - this.steamClientMock = createSteamMock(this.htmlDetailsPages); + this.historyChecks = HistoryCheck.manyFromGames(this.games); + this.steamClient = createSteamMock(htmlDetailsPages); this.steamAppsRepository = createSteamAppsRepositoryMock( - this.instantiatedApps, undefined, + this.steamApps, ); this.gamesRepository = createGamesRepositoryMock(); this.historyChecksRepository = createHistoryChecksRepositoryMock(); this.identifier = new GameIdentifier( - this.steamClientMock, + this.steamClient, this.steamAppsRepository, this.gamesRepository, this.historyChecksRepository, createLoggerMock(), createConfigMock().features, + parseHTML, ); - await this.identifier.tryViaSteamWeb(); + await this.identifier.checkIfGameViaSource(this.source); }); - it("getSteamWebUntriedFilteredSteamApps was called once", function () { + it("getSourceUntriedFilteredSteamApps was called once", function () { expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, ).toHaveBeenCalledTimes(1); }); - it("getSteamWebUntriedFilteredSteamApps was called with the correct batch size", function () { + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); }); - it("getSteamWebUntriedFilteredSteamApps was called before getSteamAppHtmlDetailsPage", function () { - expect( - this.steamAppsRepository.getSteamWebUntriedFilteredSteamApps, - ).toHaveBeenCalledBefore(this.steamClientMock.getSteamAppHtmlDetailsPage); + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); }); - it("getSteamAppHtmlDetailsPage was called four times", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledTimes( - 4, + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[0].appid, + this.source, ); - }); - - it("getSteamAppHtmlDetailsPage was called with the correct ids", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[0].appid, - ); - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[1].appid, - ); - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[2].appid, - ); - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledWith( - this.instantiatedApps[3].appid, + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[1].appid, + this.source, ); }); - it("getSteamAppHtmlDetailsPage was called before insertManyGames", function () { - expect(this.steamClientMock.getSteamAppHtmlDetailsPage).toHaveBeenCalledBefore( - this.gamesRepository.insertManyGames, - ); - }); - - it("insertManyGames was called once", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); - }); - - it("insertManyGames was called with the correct games", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); - }); - - it("insertManyGames was called before insertManyHistoryChecks", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledBefore( - this.historyChecksRepository.insertManyHistoryChecks, - ); - }); - - it("insertManyHistoryChecks was called once", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(1); - }); - - it("insertManyHistoryChecks was called with the correct history checks", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledWith(this.historychecks); + it("insertManyGames was not called", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); }); - it("insertManyHistoryChecks was called before updateSteamAppsById", function () { + it("insertManyHistoryChecks was not called", function () { expect( this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledBefore(this.steamAppsRepository.updateSteamAppsById); + ).toHaveBeenCalledTimes(0); }); it("updateSteamAppsById was called once", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); }); - it("updateSteamAppsById was called with the correct updated steam apps", function () { + it("updateSteamAppsById was called with the correct argument", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedSteamApps, + this.steamApps.content, ); }); }); - }); - }); - - describe(".tryViaSteamchartsWeb", function () { - describe("gets no steam apps from the database", function () { - beforeAll(async function () { - this.steamClientMock = createSteamMock([undefined]); - - this.steamAppsRepository = createSteamAppsRepositoryMock(undefined, []); - this.gamesRepository = createGamesRepositoryMock(); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); - - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock().features, - ); - - await this.identifier.tryViaSteamchartsWeb(); - }); - - it("getSteamchartsUntriedFilteredSteamApps was called once", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledTimes(1); - }); - - it("getSteamchartsUntriedFilteredSteamApps was called with the correct batch size", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); - }); - - it("getSteamchartsGameHtmlDetailsPage was not called", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledTimes(0); - }); - - it("insertManyGames was not called", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); - }); - - it("insertManyHistoryChecks was not called", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(0); - }); - - it("updateSteamAppsById was not called", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(0); - }); - }); - describe("gets two steam apps from the database,", function () { - describe("none of them being games", function () { + describe("Finds two unidentified steam apps in the database, one of them being a game", function () { beforeAll(async function () { - this.apps = getXSampleSteamApps(2); + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - const pages = ["", ""]; + this.source = ValidDataSources.validDataSources.steamcharts; - this.htmlDetailsPages = createHtmlDetailsPages(pages); + const htmlDetailsPages = [mortalDarknessGameHtmlDetailsPage, ""]; - this.instantiatedMarkedApps = instantiateAndMark( - this.apps, - this.htmlDetailsPages, - ); + const parsedHtmlPages = getParsedHtmlPages(htmlDetailsPages); + + this.steamApps.identifyTypes(parsedHtmlPages, this.source); - this.steamClientMock = createSteamMock(["", ""]); + this.games = this.steamApps.extractGames(parsedHtmlPages, this.source); - this.steamAppsRepository = createSteamAppsRepositoryMock(undefined, this.apps); + this.historyChecks = HistoryCheck.manyFromGames(this.games); + + this.steamClient = createSteamMock(htmlDetailsPages); + this.steamAppsRepository = createSteamAppsRepositoryMock( + undefined, + this.steamApps, + ); this.gamesRepository = createGamesRepositoryMock(); this.historyChecksRepository = createHistoryChecksRepositoryMock(); this.identifier = new GameIdentifier( - this.steamClientMock, + this.steamClient, this.steamAppsRepository, this.gamesRepository, this.historyChecksRepository, createLoggerMock(), createConfigMock().features, + parseHTML, ); - await this.identifier.tryViaSteamchartsWeb(); + await this.identifier.checkIfGameViaSource(this.source); }); - it("getSteamchartsUntriedFilteredSteamApps was called once", function () { + it("getSourceUntriedFilteredSteamApps was called once", function () { expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, ).toHaveBeenCalledTimes(1); }); - it("getSteamchartsUntriedFilteredSteamApps was called with the correct batch size", function () { + it("getSourceUntriedFilteredSteamApps was called with the correct arguments", function () { expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); + this.steamAppsRepository.getSourceUntriedFilteredSteamApps, + ).toHaveBeenCalledWith(1, this.source); }); - it("getSteamchartsUntriedFilteredSteamApps was called before getSteamchartsGameHtmlDetailsPage", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledBefore( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); + }); + + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[0].appid, + this.source, + ); + expect(this.steamClient.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.steamApps.content[1].appid, + this.source, ); }); - it("getSteamchartsGameHtmlDetailsPage was called two times", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledTimes(2); + it("insertManyGames was called once", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); }); - it("getSteamchartsGameHtmlDetailsPage was called with the correct ids", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledWith(this.apps[0].appid); + it("insertManyGames was called with the correct argument", function () { + expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); + }); + + it("insertManyHistoryChecks was called once", function () { expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledWith(this.apps[1].appid); + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledTimes(1); }); - it("getSteamchartsGameHtmlDetailsPage was called before updateSteamAppsById", function () { + it("insertManyHistoryChecks was called with the correct argument", function () { expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledBefore(this.steamAppsRepository.updateSteamAppsById); + this.historyChecksRepository.insertManyHistoryChecks, + ).toHaveBeenCalledWith(this.historyChecks); }); it("updateSteamAppsById was called once", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); }); - it("updateSteamAppsById was called with the correct steam apps", function () { + it("updateSteamAppsById was called with the correct argument", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.instantiatedMarkedApps, + this.steamApps.content, ); }); - - it("insertManyGames was not called", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(0); - }); - - it("insertManyHistoryChecks was not called", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(0); - }); - }); - }); - - describe("one of them being a game", function () { - beforeAll(async function () { - this.apps = getXSampleSteamApps(2); - - const pages = [eldenRingHttpDetailsSteamcharts, ""]; - - this.htmlDetailsPages = createHtmlDetailsPages(pages); - - this.instantiatedMarkedApps = instantiateAndMark( - this.apps, - this.htmlDetailsPages, - ); - - this.games = identifyGames(this.instantiatedMarkedApps); - - this.historychecks = HistoryCheck.manyFromGames(this.games); - - this.steamClientMock = createSteamMock([eldenRingHttpDetailsSteamcharts, ""]); - - this.steamAppsRepository = createSteamAppsRepositoryMock(undefined, this.apps); - this.gamesRepository = createGamesRepositoryMock(this.games); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); - - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock().features, - ); - - await this.identifier.tryViaSteamchartsWeb(); - }); - - it("getSteamchartsUntriedFilteredSteamApps was called once", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledTimes(1); - }); - - it("getSteamchartsUntriedFilteredSteamApps was called with the correct batch size", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledWith(1); - }); - - it("getSteamchartsUntriedFilteredSteamApps was called before getSteamchartsGameHtmlDetailsPage", function () { - expect( - this.steamAppsRepository.getSteamchartsUntriedFilteredSteamApps, - ).toHaveBeenCalledBefore(this.steamClientMock.getSteamchartsGameHtmlDetailsPage); - }); - - it("getSteamchartsGameHtmlDetailsPage was called two times", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledTimes(2); - }); - - it("getSteamchartsGameHtmlDetailsPage was called with the correct ids", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledWith(this.apps[0].appid); - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledWith(this.apps[1].appid); - }); - - it("getSteamchartsGameHtmlDetailsPage was called before insertManyGames", function () { - expect( - this.steamClientMock.getSteamchartsGameHtmlDetailsPage, - ).toHaveBeenCalledBefore(this.gamesRepository.insertManyGames); - }); - - it("insertManyGames was called once", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledTimes(1); - }); - - it("insertManyGames was called with the correct games", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledWith(this.games); - }); - - it("insertManyGames was called before insertManyHistoryChecks", function () { - expect(this.gamesRepository.insertManyGames).toHaveBeenCalledBefore( - this.historyChecksRepository.insertManyHistoryChecks, - ); - }); - - it("insertManyHistoryChecks was called once", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledTimes(1); - }); - - it("insertManyHistoryChecks was called with the correct history checks", function () { - expect(this.historyChecksRepository.insertManyHistoryChecks).toHaveBeenCalledWith( - this.historychecks, - ); - }); - - it("insertManyHistoryChecks was called before updateSteamAppsById", function () { - expect( - this.historyChecksRepository.insertManyHistoryChecks, - ).toHaveBeenCalledBefore(this.steamAppsRepository.updateSteamAppsById); - }); - - it("updateSteamAppsById was called once", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); - }); - - it("updateSteamAppsById was called with the correct steam apps", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.instantiatedMarkedApps, - ); }); }); }); @@ -719,7 +503,7 @@ describe("game.identifier.js", function () { beforeAll(async function () { this.steamClientMock = createSteamMock([]); this.steamAppsRepository = createSteamAppsRepositoryMock(); - this.gamesRepository = createGamesRepositoryMock([]); + this.gamesRepository = createGamesRepositoryMock(new GamesAggregate([])); this.historyChecksRepository = createHistoryChecksRepositoryMock(); this.identifier = new GameIdentifier( @@ -729,6 +513,7 @@ describe("game.identifier.js", function () { this.historyChecksRepository, createLoggerMock(), createConfigMock().features, + parseHTML, ); await this.identifier.updateGamesWithoutDetails(); @@ -746,40 +531,40 @@ describe("game.identifier.js", function () { expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledTimes(0); }); - it("getSteamDbHtmlDetailsPage was not called", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledTimes(0); + it("getSourceHtmlDetailsPage was not called", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(0); }); it("updateSteamAppsById was not called", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(0); }); - it("updateGameDetails was not called", function () { - expect(this.gamesRepository.updateGameDetails).toHaveBeenCalledTimes(0); + it("updateGameDetailsFrom was not called", function () { + expect(this.gamesRepository.updateGameDetailsFrom).toHaveBeenCalledTimes(0); }); }); describe("Finds two games with missing properties,", function () { beforeAll(async function () { - this.steamApps = getXSampleSteamApps(2); + this.source = ValidDataSources.validDataSources.steamDb; - this.games = getXGamesWithoutDetails(2); + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - const pages = [counterStrikeHtmlDetailsSteamDb, riskOfRainHtmlDetailsSteamDb]; + this.games = new GamesAggregate(getXGamesWithoutDetails(2)); - this.htmlDetailsPages = createHtmlDetailsPages(pages); + const htmlDetailsPages = [ + counterStrikeHtmlDetailsSteamDb, + riskOfRainHtmlDetailsSteamDb, + ]; - this.updatedApps = recordAttemptsViaSteamDb( - this.steamApps, - this.htmlDetailsPages, - ); + const parsedPages = getParsedHtmlPages(htmlDetailsPages); - this.steamClientMock = createSteamMock(pages); - this.steamAppsRepository = createSteamAppsRepositoryMock( - undefined, - undefined, - this.steamApps, - ); + this.games.updateGameDetailsFrom(parsedPages); + + this.steamApps.recordAttemptsViaSource(parsedPages, this.source); + + this.steamClientMock = createSteamMock(htmlDetailsPages); + this.steamAppsRepository = createSteamAppsRepositoryMock(this.steamApps); this.gamesRepository = createGamesRepositoryMock(this.games); this.historyChecksRepository = createHistoryChecksRepositoryMock(); @@ -790,6 +575,7 @@ describe("game.identifier.js", function () { this.historyChecksRepository, createLoggerMock(), createConfigMock().features, + parseHTML, ); await this.identifier.updateGamesWithoutDetails(); @@ -803,28 +589,18 @@ describe("game.identifier.js", function () { expect(this.gamesRepository.getGamesWithoutDetails).toHaveBeenCalledWith(1); }); - it("getGamesWithoutDetails was called before getSteamDbHtmlDetailsPage", function () { - expect(this.gamesRepository.getGamesWithoutDetails).toHaveBeenCalledBefore( - this.steamClientMock.getSteamDbHtmlDetailsPage, - ); - }); - - it("getSteamDbHtmlDetailsPage was called twice", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledTimes(2); + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); }); - it("getSteamDbHtmlDetailsPage was called with the correct ids", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[0].id, + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.games.content[0].id, + this.source, ); - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[1].id, - ); - }); - - it("getSteamDbHtmlDetailsPage was called before updateSteamAppsById", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledBefore( - this.steamAppsRepository.updateSteamAppsById, + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.games.content[1].id, + this.source, ); }); @@ -834,22 +610,18 @@ describe("game.identifier.js", function () { it("updateSteamAppsById was called with the correct argument", function () { expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedApps, - ); - }); - - it("updateSteamAppsById was called before updateSteamAppsById", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledBefore( - this.gamesRepository.updateGameDetails, + this.steamApps.content, ); }); - it("updateGameDetails was called once", function () { - expect(this.gamesRepository.updateGameDetails).toHaveBeenCalledTimes(1); + it("updateGameDetailsFrom was called once", function () { + expect(this.gamesRepository.updateGameDetailsFrom).toHaveBeenCalledTimes(1); }); - it("updateGameDetails was called with the correct argument", function () { - expect(this.gamesRepository.updateGameDetails).toHaveBeenCalledWith(this.games); + it("updateGameDetailsFrom was called with the correct argument", function () { + expect(this.gamesRepository.updateGameDetailsFrom).toHaveBeenCalledWith( + this.games.content, + ); }); }); }); @@ -859,7 +631,7 @@ describe("game.identifier.js", function () { beforeAll(async function () { this.steamClientMock = createSteamMock([]); this.steamAppsRepository = createSteamAppsRepositoryMock(); - this.gamesRepository = createGamesRepositoryMock([]); + this.gamesRepository = createGamesRepositoryMock(new GamesAggregate([])); this.historyChecksRepository = createHistoryChecksRepositoryMock(); this.identifier = new GameIdentifier( @@ -869,6 +641,7 @@ describe("game.identifier.js", function () { this.historyChecksRepository, createLoggerMock(), createConfigMock(2).features, + parseHTML, ); await this.identifier.updateGamesWithoutReleaseDates(); @@ -886,8 +659,8 @@ describe("game.identifier.js", function () { expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledTimes(0); }); - it("getSteamDbHtmlDetailsPage was not called", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledTimes(0); + it("getSourceHtmlDetailsPage was not called", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(0); }); it("updateSteamAppsById was not called", function () { @@ -900,265 +673,111 @@ describe("game.identifier.js", function () { }); describe("Finds two games with missing release dates", function () { - describe("with no empty html pages", function () { - beforeAll(async function () { - this.games = getXGamesWithoutDetails(2); - - const apps = getXSampleSteamApps(2); - - const pages = [counterStrikeHtmlDetailsSteamDb, riskOfRainHtmlDetailsSteamDb]; - - const updatedPages = createHtmlDetailsPages(pages); - - this.updatedApps = recordAttemptsViaSteamDb(apps, updatedPages); - - this.steamClientMock = createSteamMock([ - counterStrikeHtmlDetailsSteamDb, - riskOfRainHtmlDetailsSteamDb, - ]); - this.steamAppsRepository = createSteamAppsRepositoryMock( - undefined, - undefined, - apps, - ); - this.gamesRepository = createGamesRepositoryMock(this.games); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); - - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock(2).features, - ); - - await this.identifier.updateGamesWithoutReleaseDates(); - }); - - it("getGamesWithoutReleaseDates was called once", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledTimes( - 1, - ); - }); - - it("getGamesWithoutReleaseDates was called with the correct batch size", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledWith( - 2, - ); - }); - - it("getGamesWithoutReleaseDates was called before getSteamAppsById", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledBefore( - this.steamAppsRepository.getSteamAppsById, - ); - }); - - it("getSteamAppsById was called once", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledTimes(1); - }); - - it("getSteamAppsById was called with the correct argument", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledWith( - this.games.map((game) => game.id), - ); - }); + beforeAll(async function () { + this.games = new GamesAggregate(getXGamesWithoutDetails(2)); - it("getSteamAppsById was called before getSteamDbHtmlDetailsPage", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledBefore( - this.steamClientMock.getSteamDbHtmlDetailsPage, - ); - }); + this.steamApps = new SteamAppsAggregate(getXSampleSteamApps(2)); - it("getSteamDbHtmlDetailsPage was called twice", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledTimes(2); - }); + const htmlDetailsPages = [ + counterStrikeHtmlDetailsSteamDb, + riskOfRainHtmlDetailsSteamDb, + ]; - it("getSteamDbHtmlDetailsPage was called with the correct ids", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[0].id, - ); - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[1].id, - ); - }); + const parsedPages = getParsedHtmlPages(htmlDetailsPages); - it("getSteamDbHtmlDetailsPage was called before updateSteamAppsById", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledBefore( - this.steamAppsRepository.updateSteamAppsById, - ); - }); + this.source = ValidDataSources.validDataSources.steamDb; - it("updateSteamAppsById was called once", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); - }); + this.steamApps.recordAttemptsViaSource(parsedPages, this.source); - it("updateSteamAppsById was called with the correct argument", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedApps, - ); - }); + this.games.extractReleaseDatesFrom(parsedPages); - it("updateSteamAppsById was called before updateReleaseDates", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledBefore( - this.gamesRepository.updateReleaseDates, - ); - }); + this.steamClientMock = createSteamMock(htmlDetailsPages); + this.steamAppsRepository = createSteamAppsRepositoryMock(this.steamApps); + this.gamesRepository = createGamesRepositoryMock(this.games); + this.historyChecksRepository = createHistoryChecksRepositoryMock(); - it("updateReleaseDates was called once", function () { - expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledTimes(1); - }); + this.identifier = new GameIdentifier( + this.steamClientMock, + this.steamAppsRepository, + this.gamesRepository, + this.historyChecksRepository, + createLoggerMock(), + createConfigMock(2).features, + parseHTML, + ); - it("updateReleaseDates was called with the correct argument", function () { - expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledWith( - this.games, - ); - }); + await this.identifier.updateGamesWithoutReleaseDates(); }); - describe("with one empty html page", function () { - beforeAll(async function () { - const steamApps = getXSampleSteamApps(2); - - this.games = getXGamesWithoutDetails(2); - - const pages = [counterStrikeHtmlDetailsSteamDb, ""]; - - this.steamClientMock = createSteamMock(pages); - - const htmlPages = createHtmlDetailsPages(pages); - - this.updatedApps = recordAttemptsViaSteamDb(steamApps, htmlPages); - - updateMissingReleaseDates(this.games, htmlPages); - - this.steamAppsRepository = createSteamAppsRepositoryMock( - undefined, - undefined, - steamApps, - ); - this.gamesRepository = createGamesRepositoryMock(this.games); - this.historyChecksRepository = createHistoryChecksRepositoryMock(); - - this.identifier = new GameIdentifier( - this.steamClientMock, - this.steamAppsRepository, - this.gamesRepository, - this.historyChecksRepository, - createLoggerMock(), - createConfigMock(2).features, - ); - - await this.identifier.updateGamesWithoutReleaseDates(); - }); - - it("getGamesWithoutReleaseDates was called once", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledTimes( - 1, - ); - }); - - it("getGamesWithoutReleaseDates was called with the correct batch size", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledWith( - 2, - ); - }); - - it("getGamesWithoutReleaseDates was called before getSteamAppsById", function () { - expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledBefore( - this.steamAppsRepository.getSteamAppsById, - ); - }); - - it("getSteamAppsById was called once", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledTimes(1); - }); - - it("getSteamAppsById was called with the correct argument", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledWith( - this.games.map((game) => game.id), - ); - }); + it("getGamesWithoutReleaseDates was called once", function () { + expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledTimes(1); + }); - it("getSteamAppsById was called before getSteamDbHtmlDetailsPage", function () { - expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledBefore( - this.steamClientMock.getSteamDbHtmlDetailsPage, - ); - }); + it("getGamesWithoutReleaseDates was called with the correct batch size", function () { + expect(this.gamesRepository.getGamesWithoutReleaseDates).toHaveBeenCalledWith(2); + }); - it("getSteamDbHtmlDetailsPage was called twice", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledTimes(2); - }); + it("getSteamAppsById was called once", function () { + expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledTimes(1); + }); - it("getSteamDbHtmlDetailsPage was called with the correct ids", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[0].id, - ); - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledWith( - this.games[1].id, - ); - }); + it("getSteamAppsById was called with the correct argument", function () { + expect(this.steamAppsRepository.getSteamAppsById).toHaveBeenCalledWith( + this.games.ids, + ); + }); - it("getSteamDbHtmlDetailsPage was called before updateSteamAppsById", function () { - expect(this.steamClientMock.getSteamDbHtmlDetailsPage).toHaveBeenCalledBefore( - this.steamAppsRepository.updateSteamAppsById, - ); - }); + it("getSourceHtmlDetailsPage was called twice", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledTimes(2); + }); - it("updateSteamAppsById was called once", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); - }); + it("getSourceHtmlDetailsPage was called with the correct arguments", function () { + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.games.content[0].id, + this.source, + ); + expect(this.steamClientMock.getSourceHtmlDetailsPage).toHaveBeenCalledWith( + this.games.content[1].id, + this.source, + ); + }); - it("updateSteamAppsById was called with the correct argument", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( - this.updatedApps, - ); - }); + it("updateSteamAppsById was called once", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledTimes(1); + }); - it("updateSteamAppsById was called before updateReleaseDates", function () { - expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledBefore( - this.gamesRepository.updateReleaseDates, - ); - }); + it("updateSteamAppsById was called with the correct argument", function () { + expect(this.steamAppsRepository.updateSteamAppsById).toHaveBeenCalledWith( + this.steamApps.content, + ); + }); - it("updateReleaseDates was called once", function () { - expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledTimes(1); - }); + it("updateReleaseDates was called once", function () { + expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledTimes(1); + }); - it("updateReleaseDates was called with the correct argument", function () { - expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledWith( - this.games, - ); - }); + it("updateReleaseDates was called with the correct argument", function () { + expect(this.gamesRepository.updateReleaseDates).toHaveBeenCalledWith( + this.games.content, + ); }); }); }); }); function createSteamMock(args) { - const spyObj = jasmine.createSpyObj("steamClient", [ - "getSteamAppHtmlDetailsPage", - "getSteamchartsGameHtmlDetailsPage", - "getSteamDbHtmlDetailsPage", - ]); + const spyObj = jasmine.createSpyObj("steamClient", ["getSourceHtmlDetailsPage"]); - spyObj.getSteamAppHtmlDetailsPage.and.returnValues(...args); - spyObj.getSteamchartsGameHtmlDetailsPage.and.returnValues(...args); - spyObj.getSteamDbHtmlDetailsPage.and.returnValues(...args); + spyObj.getSourceHtmlDetailsPage.and.returnValues(...args); return spyObj; } -function createSteamAppsRepositoryMock( - steamWebDbRet, - steamchartsWebDbRet, - steamAppByIdDbRet, -) { +function createSteamAppsRepositoryMock(steamAppByIdDbRet, sourceUntriedRet) { return jasmine.createSpyObj("SteamAppsRepository", { - getSteamWebUntriedFilteredSteamApps: Promise.resolve(steamWebDbRet), updateSteamAppsById: Promise.resolve(undefined), - getSteamchartsUntriedFilteredSteamApps: Promise.resolve(steamchartsWebDbRet), getSteamAppsById: Promise.resolve(steamAppByIdDbRet), + getSourceUntriedFilteredSteamApps: Promise.resolve(sourceUntriedRet), }); } @@ -1166,9 +785,10 @@ function createGamesRepositoryMock(gamesRepoRet) { return jasmine.createSpyObj("GamesRepository", { insertManyGames: Promise.resolve(undefined), getGamesWithoutDetails: Promise.resolve(gamesRepoRet), - updateGameDetails: Promise.resolve(undefined), + updateGameDetailsFrom: Promise.resolve(undefined), getGamesWithoutReleaseDates: Promise.resolve(gamesRepoRet), updateReleaseDates: Promise.resolve(undefined), + getSourceHtmlDetailsPage: Promise.resolve(gamesRepoRet), }); } @@ -1177,19 +797,3 @@ function createHistoryChecksRepositoryMock() { insertManyHistoryChecks: Promise.resolve(undefined), }); } - -function instantiateAndMark(apps, pages) { - const instantiatedApps = SteamApp.manyFromSteamApi(apps); - - const updatedApps = instantiatedApps.map((app, i) => { - if (pages[i].page === "") app.failedViaSteamchartsWeb(); - if (pages[i].page) { - app.type = SteamApp.validTypes.game; - } - app.triedViaSteamchartsWeb(); - - return app; - }); - - return updatedApps; -} diff --git a/backend/src/core/models/game.js b/backend/src/core/models/game.js index b91b0c807..4292113a9 100644 --- a/backend/src/core/models/game.js +++ b/backend/src/core/models/game.js @@ -11,17 +11,32 @@ export class Game { imageUrl; playerHistory; + copy() { + const copy = new Game(); + copy.id = this.id; + copy.name = this.name; + copy.releaseDate = this.releaseDate; + copy.developers = this.developers.slice(); + copy.genres = this.genres.slice(); + copy.description = this.description; + copy.imageUrl = this.imageUrl; + copy.playerHistory = PlayerHistory.manyFromDbEntry(this.playerHistory); + + return copy; + } + // prettier-ignore - static fromSteamApp(steamApp, releaseDate, developers, genres, description) { + static fromSteamApp(steamApp, page) { const game = new Game(); game.id = steamApp.appid; game.name = steamApp.name; - game.releaseDate = releaseDate; - game.developers = developers; - game.genres = genres; - game.description = description; + game.releaseDate = game.#extractReleaseDateFrom(page); + game.developers = game.#extractDevelopersFrom(page); + game.genres = game.#extractGenresFrom(page); + game.description = game.#extractDescriptionFrom(page); game.imageUrl = `https://cdn.akamai.steamstatic.com/steam/apps/${game.id}/header.jpg` game.playerHistory = []; + return game; } @@ -93,19 +108,124 @@ export class Game { }); } - updateGameDetails(newDevelopers, newGenres, newDescription) { - if (newDevelopers.length !== 0 && this.developers.length === 0) - this.developers = Array.from(newDevelopers); + #extractReleaseDateFrom(page) { + const releaseDateElement = page.querySelector(".release_date .date"); + + if (!releaseDateElement) return ""; + + const releaseDate = new Date(`${releaseDateElement.textContent.trim()} UTC`); + + return releaseDate == "Invalid Date" ? "" : releaseDate; + } + + #extractDevelopersFrom(page) { + const developers = page.querySelector(".dev_row #developers_list"); + + if (!developers) return []; + + return Array.from(developers.children).map((developer) => + developer.textContent.trim(), + ); + } + + #extractGenresFrom(page) { + const genres = page.querySelector("#genresAndManufacturer span"); + + if (!genres) return []; + + return Array.from(genres.children) + .map((genre) => genre.textContent.trim()) + .filter((genre) => !!genre); + } + + #extractDescriptionFrom(page) { + const description = page.querySelector(".game_description_snippet"); + + if (!description) return ""; + + return description.textContent.trim(); + } + + updateGameDetailsFrom(page) { + this.#updateDevelopers(page); + this.#updateGenres(page); + this.#updateDescription(page); + } + + #updateDevelopers(page) { + if (this.developers.length !== 0) return; + + this.developers = this.#extractSteamDbDevelopersFrom(page); + } + + #updateGenres(page) { + if (this.genres.length !== 0) return; + + this.genres = this.#extractSteamDbGenresFrom(page); + } + + #updateDescription(page) { + if (this.description.length !== 0) return; + + this.description = this.#extractSteamDbDescriptionFrom(page); + } + + updateReleaseDate(page) { + if (this.releaseDate) return; + + const date = this.#extractSteamDbReleaseDateFrom(page); + + if (date === "") return; + + this.releaseDate = date; + } + + #extractSteamDbDevelopersFrom(page) { + const developers = page.querySelector( + "table.table.table-bordered.table-hover.table-responsive-flex tbody tr:nth-child(3) td:last-child", + ); + + if (!developers) return []; + + return Array.from(developers.children).map((developer) => developer.textContent); + } + + #extractSteamDbGenresFrom(page) { + const domTableBody = page.querySelector("#info tbody"); + + if (!domTableBody) return []; + + const genresNodes = Array.from(domTableBody.children).filter( + (tableEntry) => tableEntry.children[0].textContent === "Store Genres", + )[0].children[1].childNodes; + + return Array.from(genresNodes) + .filter((genre) => genre.constructor.name === "Text") + .map((genre) => genre.nodeValue.replace(",", "").trim()); + } + + #extractSteamDbDescriptionFrom(page) { + const description = page.querySelector(".header-description"); - if (newGenres.length !== 0 && this.genres.length === 0) - this.genres = Array.from(newGenres); + if (!description) return ""; - if (newDescription !== "" && this.description === "") - this.description = newDescription; + return description.textContent; } - updateReleaseDate(newReleaseDate) { - if (newReleaseDate !== "" && this.releaseDate === "") - this.releaseDate = newReleaseDate; + #extractSteamDbReleaseDateFrom(page) { + const releaseDateElement = page.querySelector( + "table.table.table-bordered.table-hover.table-responsive-flex tbody tr:last-child td:last-child", + ); + + if (!releaseDateElement) return ""; + + const releaseDateString = releaseDateElement.textContent; + + const releaseDate = new Date(` + ${releaseDateString.slice(0, releaseDateString.indexOf("–") - 1)} UTC`); + + if (releaseDate == "Invalid Date") return ""; + + return releaseDate; } } diff --git a/backend/src/core/models/game.mocks.js b/backend/src/core/models/game.mocks.js index 244b15f2e..21f9b882f 100644 --- a/backend/src/core/models/game.mocks.js +++ b/backend/src/core/models/game.mocks.js @@ -1,22 +1,24 @@ +import { getParsedHtmlPage } from "../../../assets/html.details.pages.mock.js"; +import { feartressGameHtmlDetailsPage } from "../../../assets/steam-details-pages/feartress.game.html.details.page.js"; import { daysToMs } from "../../common/time.utils.js"; import { Game } from "./game.js"; +import { getSamplePlayerHistory } from "./player.history.mocks.js"; import { getXSampleSteamApps } from "./steam.app.mocks.js"; export const getXGamesWithoutDetails = (amount) => { const steamApps = getXSampleSteamApps(amount); - return steamApps.map((app, i) => Game.fromSteamApp(steamApps[i], "", [], [], "")); + return steamApps.map((app, i) => + Game.fromSteamApp(steamApps[i], getParsedHtmlPage("")), + ); }; export const getOneSteamAppInstantiatedGame = () => { const steamApp = getXSampleSteamApps(1); - const releaseDate = "21 July 2019"; - const developers = ["Valve", "Hopoo Games"]; - const genres = ["Action", "Adventure"]; - const description = "Best game"; + const page = getParsedHtmlPage(feartressGameHtmlDetailsPage); - return Game.fromSteamApp(steamApp, releaseDate, developers, genres, description); + return Game.fromSteamApp(steamApp, page); }; export const getXsteamchartsInstantiatedGames = (amount) => { @@ -38,6 +40,22 @@ export const getOneGameWithDetails = () => { ]; }; +export const getOneGameWithPlayerHistory = () => { + const dbEntry = [ + { + id: 239140, + name: "Dying Light", + releaseDate: "21.09.1989", + developers: ["Techland"], + genres: ["Action", "RPG"], + description: "Best game", + playerHistory: getSamplePlayerHistory(), + }, + ]; + + return Game.manyFromDbEntry(dbEntry)[0]; +}; + export const getGamesWithEmptyPlayerHistories = () => { return [ { diff --git a/backend/src/core/models/game.spec.js b/backend/src/core/models/game.spec.js index b943db7de..ca1e724a2 100644 --- a/backend/src/core/models/game.spec.js +++ b/backend/src/core/models/game.spec.js @@ -1,583 +1,690 @@ +import { getParsedHtmlPage } from "../../../assets/html.details.pages.mock.js"; +import { crusaderKingsDetailsPage } from "../../../assets/steam-details-pages/crusader.kings.multiple.developers.html.details.page.js"; +import { feartressGameHtmlDetailsPage } from "../../../assets/steam-details-pages/feartress.game.html.details.page.js"; +import { mortalDarknessGameHtmlDetailsPage } from "../../../assets/steam-details-pages/mortal.darkness.game.html.details.page.js"; +import { riskOfRainHtmlDetailsPageMissingInfo } from "../../../assets/steam-details-pages/risk.of.rain.missing.additional.info.page.js"; +import { counterStrikeHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/counter.strike.html.details.page.js"; +import { karmazooHtmlDetailsPageSteamDb } from "../../../assets/steamdb-details-pages/karmazoo.html.details.page.js"; +import { riskOfRainHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/risk.of.rain.html.details.page.js"; import { Game } from "./game.js"; -import { getOneSteamAppInstantiatedGame, getXGamesWithoutDetails } from "./game.mocks.js"; +import { getOneGameWithPlayerHistory, getXGamesWithoutDetails } from "./game.mocks.js"; import { PlayerHistory } from "./player.history.js"; import { getXSampleSteamApps } from "./steam.app.mocks.js"; -describe("game.js", function () { - describe("Game", function () { - describe(".fromSteamApp", function () { - describe("is called with no arguments, ", function () { - it("an Error is thrown", function () { - expect(Game.fromSteamApp).toThrowError(); - }); +describe("Game", function () { + describe(".copy", function () { + beforeAll(function () { + this.game = getOneGameWithPlayerHistory(); + this.result = this.game.copy(); + + this.game.id = 99; + }); + + it("the result is a copy of game", function () { + expect(this.result).toBeInstanceOf(Game); + expect(this.result.playerHistory[0]).toBeInstanceOf(PlayerHistory); + }); + + it("the copy has identical values to the original", function () { + expect(this.result.id).toBe(239140); + expect(this.result.name).toBe("Dying Light"); + expect(this.result.releaseDate).toBe("21.09.1989"); + expect(this.result.developers).toEqual(["Techland"]); + expect(this.result.genres).toEqual(["Action", "RPG"]); + expect(this.result.description).toBe("Best game"); + }); + }); + + describe(".fromSteamApp", function () { + describe("when the method is called", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(feartressGameHtmlDetailsPage); + + this.result = Game.fromSteamApp(steamApp, page); }); - describe("is called with incomplete arguments, ", function () { - beforeAll(function () { - this.testObject = { - id: 123, - name: "test game", - }; - }); + it("is an instance of Game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("an Error is thrown", function () { - expect(Game.fromSteamApp.bind(this.testObject)).toThrowError(); - }); + it("the game has the correct values", function () { + expect(this.result.id).toBe(1); + expect(this.result.name).toBe("Game #1"); + expect(this.result.releaseDate).toEqual(new Date("Jan 1 2001 UTC")); + expect(this.result.developers).toEqual(["Frederik List", "Aaron Miles"]); + expect(this.result.imageUrl).toBe( + `https://cdn.akamai.steamstatic.com/steam/apps/1/header.jpg`, + ); + expect(this.result.playerHistory).toEqual([]); }); + }); - describe("is called with appropriate attributes, the returned value", function () { - beforeAll(function () { - this.testObject = { - appid: 123, - name: "test game", - imageUrl: "test url", - playerHistory: [], - }; + describe("if the provided HTML page does not include a release date section,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - this.releaseDate = "24.08.2023"; + this.result = Game.fromSteamApp(steamApp, page); + }); - this.developers = ["Two Giants Studios"]; + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - this.genres = ["Action", "Adventure"]; + it("the game's release date will be an empty string", function () { + expect(this.result.releaseDate).toBe(""); + }); + }); - this.description = ["A game description"]; + describe("if the provided HTML page includes a release date,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(mortalDarknessGameHtmlDetailsPage); - this.result = Game.fromSteamApp( - this.testObject, - this.releaseDate, - this.developers, - this.genres, - this.description, - ); - }); + this.result = Game.fromSteamApp(steamApp, page); + }); - it("is an instance of Game", function () { - expect(this.result).toBeInstanceOf(Game); - }); + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has an 'id' property which equals 123", function () { - expect(this.result.id).toBe(this.testObject.appid); - }); + it("the game's release date is set to the correct date'", function () { + expect(this.result.releaseDate.toISOString()).toEqual("2023-08-01T00:00:00.000Z"); + }); + }); - it("has a 'name' property which equals 'test game'", function () { - expect(this.result.name).toBe(this.testObject.name); - }); + describe("if the provided HTML page does not include any developers,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - it("has a 'releaseDate' property which equals '24.08.2023", function () { - expect(this.result.releaseDate).toBe(this.releaseDate); - }); + this.result = Game.fromSteamApp(steamApp, page); + }); - it("has a 'developers' property which is an array with a length of 1", function () { - expect(this.result.developers.length).toBe(1); - }); + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has a 'developers' property which equals 'Two Giants Studios'", function () { - expect(this.result.developers[0]).toBe(this.developers[0]); - }); + it("the games developers do not get updated", function () { + expect(this.result.developers).toEqual([]); + }); + }); - it("has a 'genres' property which is an array with a length of 2", function () { - expect(this.result.genres.length).toBe(2); - }); + describe("if the provided HTML page includes two developers,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(crusaderKingsDetailsPage); - it("'s first value of the genres property equals 'Action'", function () { - expect(this.result.genres[0]).toBe(this.genres[0]); - }); + this.result = Game.fromSteamApp(steamApp, page); + }); - it("'s second value of the genres property equals 'Adventure'", function () { - expect(this.result.genres[1]).toBe(this.genres[1]); - }); + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has a 'description' property which equals 'A game description'", function () { - expect(this.result.description).toBe(this.description); - }); + it("the game's developers get updated with the correct values", function () { + expect(this.result.developers).toEqual([ + "Paradox Development Studio", + "Paradox Thalassic", + ]); + }); + }); - it("has an 'imageUrl' property which equals a link", function () { - expect(this.result.imageUrl).toBe( - `https://cdn.akamai.steamstatic.com/steam/apps/${this.testObject.appid}/header.jpg`, - ); - }); + describe("if the provided HTML page does not include any genres,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - it("has a 'playerHistory' property which equals an empty array", function () { - expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); - }); + this.result = Game.fromSteamApp(steamApp, page); }); - describe("is called with appropriate attributes, but the releaseDate and Developers are empty the returned value", function () { - beforeAll(function () { - this.testObject = { - appid: 123, - name: "test game", - imageUrl: "test url", - playerHistory: [], - }; + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); + + it("the game's genres do not get updated", function () { + expect(this.result.genres).toEqual([]); + }); + }); - this.releaseDate = ""; - this.developers = []; + describe("if the provided HTML page includes genres,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(mortalDarknessGameHtmlDetailsPage); - this.result = Game.fromSteamApp( - this.testObject, - this.releaseDate, - this.developers, - ); - }); + this.result = Game.fromSteamApp(steamApp, page); + }); - it("is an instance of Game", function () { - expect(this.result).toBeInstanceOf(Game); - }); + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has an 'id' property which equals 123", function () { - expect(this.result.id).toBe(this.testObject.appid); - }); + it("the game's genres get updated with the correct values", function () { + expect(this.result.genres).toEqual(["Action", "Adventure", "Indie", "RPG"]); + }); + }); - it("has a 'name' property which equals 'test game'", function () { - expect(this.result.name).toBe(this.testObject.name); - }); + describe("if the provided HTML page does not include a game description,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - it("has a 'releaseDate' property which equals an empty string", function () { - expect(this.result.releaseDate).toBe(""); - }); + this.result = Game.fromSteamApp(steamApp, page); + }); - it("has a 'developers' property which equals an empty array", function () { - expect(this.result.developers).toEqual([]); - }); + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has an 'imageUrl' property which equals a link", function () { - expect(this.result.imageUrl).toBe( - `https://cdn.akamai.steamstatic.com/steam/apps/${this.testObject.appid}/header.jpg`, - ); - }); + it("the game's description does not get updated", function () { + expect(this.result.description).toEqual(""); + }); + }); - it("has a 'playerHistory' property which equals an empty array", function () { - expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); - }); + describe("if the provided HTML page includes a description,", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1)[0]; + const page = getParsedHtmlPage(mortalDarknessGameHtmlDetailsPage); + + this.result = Game.fromSteamApp(steamApp, page); + }); + + it("the result is an instance of game", function () { + expect(this.result).toBeInstanceOf(Game); + }); + + it("the game's description is updated with the correct value", function () { + expect(this.result.description).toBe( + "“One grim dawn and noble I wake, The darkness is rampant, our oath shall break. A noble warrior soon shall rise, and clear the air of the darkened skies.”", + ); }); }); + }); - describe(".fromSteamcharts", function () { - describe("is called with no arguments, ", function () { - it("an Error is thrown", function () { - expect(Game.fromSteamcharts).toThrowError(); - }); + describe(".fromSteamcharts", function () { + describe("is called with no arguments, ", function () { + it("an Error is thrown", function () { + expect(Game.fromSteamcharts).toThrowError(); }); + }); - describe("is called with incomplete arguments, ", function () { - beforeAll(function () { - this.testObject = { - id: 123, - name: "test game", - }; - }); + describe("is called with incomplete arguments, ", function () { + beforeAll(function () { + this.testObject = { + id: 123, + name: "test game", + }; + }); - it("an Error is thrown", function () { - expect(Game.fromSteamcharts.bind(this.testObject)).toThrowError(); - }); + it("an Error is thrown", function () { + expect(Game.fromSteamcharts.bind(this.testObject)).toThrowError(); }); + }); - describe("is called with appropriate attributes, the returned value", function () { - beforeAll(function () { - this.testObject = { - appid: 123, - name: "test game", - imageUrl: "test url", - playerHistory: [], - }; + describe("is called with appropriate attributes, the returned value", function () { + beforeAll(function () { + this.testObject = { + appid: 123, + name: "test game", + imageUrl: "test url", + playerHistory: [], + }; - this.result = Game.fromSteamcharts(this.testObject); - }); + this.result = Game.fromSteamcharts(this.testObject); + }); - it("is an instance of Game", function () { - expect(this.result).toBeInstanceOf(Game); - }); + it("is an instance of Game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has an 'id' property which equals 123", function () { - expect(this.result.id).toBe(this.testObject.appid); - }); + it("has an 'id' property which equals 123", function () { + expect(this.result.id).toBe(this.testObject.appid); + }); - it("has a 'name' property which equals 'test game'", function () { - expect(this.result.name).toBe(this.testObject.name); - }); + it("has a 'name' property which equals 'test game'", function () { + expect(this.result.name).toBe(this.testObject.name); + }); - it("has a 'releaseDate' property which equals an empty string", function () { - expect(this.result.releaseDate).toBe(""); - }); + it("has a 'releaseDate' property which equals an empty string", function () { + expect(this.result.releaseDate).toBe(""); + }); - it("has a 'developers' property which equals an empty array", function () { - expect(this.result.developers).toEqual([]); - }); + it("has a 'developers' property which equals an empty array", function () { + expect(this.result.developers).toEqual([]); + }); - it("has a 'genres' property which equals an empty array", function () { - expect(this.result.genres).toEqual([]); - }); + it("has a 'genres' property which equals an empty array", function () { + expect(this.result.genres).toEqual([]); + }); - it("has a 'description' property which equals an empty string", function () { - expect(this.result.description).toEqual(""); - }); + it("has a 'description' property which equals an empty string", function () { + expect(this.result.description).toEqual(""); + }); - it("has an 'imageUrl' property which equals a link", function () { - expect(this.result.imageUrl).toBe( - `https://cdn.akamai.steamstatic.com/steam/apps/${this.testObject.appid}/header.jpg`, - ); - }); + it("has an 'imageUrl' property which equals a link", function () { + expect(this.result.imageUrl).toBe( + `https://cdn.akamai.steamstatic.com/steam/apps/${this.testObject.appid}/header.jpg`, + ); + }); - it("has a 'playerHistory' property which equals an empty array", function () { - expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); - }); + it("has a 'playerHistory' property which equals an empty array", function () { + expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); }); }); + }); - describe(".fromDbEntry", function () { - describe("is called with no arguments, ", function () { - it("an Error is thrown", function () { - expect(Game.fromDbEntry).toThrowError(); - }); + describe(".fromDbEntry", function () { + describe("is called with no arguments, ", function () { + it("an Error is thrown", function () { + expect(Game.fromDbEntry).toThrowError(); }); + }); - describe("is called with incomplete arguments, ", function () { - beforeAll(function () { - this.testObject = { - id: 123, - name: "test game", - }; - }); + describe("is called with incomplete arguments, ", function () { + beforeAll(function () { + this.testObject = { + id: 123, + name: "test game", + }; + }); - it("an Error is thrown", function () { - expect(Game.fromDbEntry.bind(this.testObject)).toThrowError(); - }); + it("an Error is thrown", function () { + expect(Game.fromDbEntry.bind(this.testObject)).toThrowError(); }); + }); - describe("is called with appropriate attributes, the returned value", function () { - beforeAll(function () { - this.testObject = { - id: 123, - name: "test game", - releaseDate: "3 Mar, 2022", - developers: ["Crossplatform"], - genres: ["Action", "Adventure"], - description: "A game's description", - imageUrl: "test url", - playerHistory: [], - }; + describe("is called with appropriate attributes, the returned value", function () { + beforeAll(function () { + this.testObject = { + id: 123, + name: "test game", + releaseDate: "3 Mar, 2022", + developers: ["Crossplatform"], + genres: ["Action", "Adventure"], + description: "A game's description", + imageUrl: "test url", + playerHistory: [], + }; - this.result = Game.fromDbEntry(this.testObject); - }); + this.result = Game.fromDbEntry(this.testObject); + }); - it("is an instance of Game", function () { - expect(this.result).toBeInstanceOf(Game); - }); + it("is an instance of Game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("has an 'id' property which equals 123", function () { - expect(this.result.id).toBe(this.testObject.id); - }); + it("has an 'id' property which equals 123", function () { + expect(this.result.id).toBe(this.testObject.id); + }); - it("has a 'name' property which equals 'test game'", function () { - expect(this.result.name).toBe(this.testObject.name); - }); + it("has a 'name' property which equals 'test game'", function () { + expect(this.result.name).toBe(this.testObject.name); + }); - it("has a 'releaseDate' property which equals '3 Mar, 2022", function () { - expect(this.result.releaseDate).toBe("3 Mar, 2022"); - }); + it("has a 'releaseDate' property which equals '3 Mar, 2022", function () { + expect(this.result.releaseDate).toBe("3 Mar, 2022"); + }); - it("has a 'developers' property which equals 'Crossplatform", function () { - expect(this.result.developers[0]).toBe("Crossplatform"); - }); + it("has a 'developers' property which equals 'Crossplatform", function () { + expect(this.result.developers[0]).toBe("Crossplatform"); + }); - it("has a 'genres' property which is an array with a length of 2", function () { - expect(this.result.genres.length).toBe(2); - }); + it("has a 'genres' property which is an array with a length of 2", function () { + expect(this.result.genres.length).toBe(2); + }); - it("first genres value equals 'Action'", function () { - expect(this.result.genres[0]).toBe("Action"); - }); + it("first genres value equals 'Action'", function () { + expect(this.result.genres[0]).toBe("Action"); + }); - it("second genres value equals 'Adventure'", function () { - expect(this.result.genres[1]).toBe("Adventure"); - }); + it("second genres value equals 'Adventure'", function () { + expect(this.result.genres[1]).toBe("Adventure"); + }); - it("has a 'description' property which equals 'A game's description'", function () { - expect(this.result.description).toBe("A game's description"); - }); + it("has a 'description' property which equals 'A game's description'", function () { + expect(this.result.description).toBe("A game's description"); + }); - it("has an 'imageUrl' property which equals 'test url'", function () { - expect(this.result.imageUrl).toBe(this.testObject.imageUrl); - }); + it("has an 'imageUrl' property which equals 'test url'", function () { + expect(this.result.imageUrl).toBe(this.testObject.imageUrl); + }); - it("has a 'playerHistory' property which equals an empty array", function () { - expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); - }); + it("has a 'playerHistory' property which equals an empty array", function () { + expect(this.result.playerHistory).toEqual(this.testObject.playerHistory); }); }); + }); - describe(".pushCurrentPlayers creates a new player history entry or updates an existing one.", function () { - describe("When this month's player history entry already exists,", function () { - describe("players are added to the existing entry.", function () { - beforeAll(function () { - this.currentPlayers = 45; + describe(".pushCurrentPlayers creates a new player history entry or updates an existing one.", function () { + describe("When this month's player history entry already exists,", function () { + describe("players are added to the existing entry.", function () { + beforeAll(function () { + this.currentPlayers = 45; + + const playerHistory = [ + { + year: new Date().getFullYear(), + month: new Date().getMonth(), + averagePlayers: 0, + trackedPlayers: [], + }, + ]; + + this.historyLength = playerHistory.length; + + const game = { + id: 1, + name: "Test Game", + playerHistory: PlayerHistory.manyFromDbEntry(playerHistory), + }; - const playerHistory = [ - { - year: new Date().getFullYear(), - month: new Date().getMonth(), - averagePlayers: 0, - trackedPlayers: [], - }, - ]; + this.result = Game.fromDbEntry(game); - this.historyLength = playerHistory.length; + this.result.pushCurrentPlayers(this.currentPlayers); + }); - const game = { - id: 1, - name: "Test Game", - playerHistory: PlayerHistory.manyFromDbEntry(playerHistory), - }; + it("No new entry is created", function () { + expect(this.result.playerHistory.length).toBe(this.historyLength); + }); - this.result = Game.fromDbEntry(game); + it("The existing entry is updated.", function () { + expect(this.result.playerHistory[0]).toBeInstanceOf(PlayerHistory); + expect(this.result.playerHistory[0].trackedPlayers[0].players).toBe( + this.currentPlayers, + ); + }); + }); + }); - this.result.pushCurrentPlayers(this.currentPlayers); - }); + describe("When this month's player history entry does not exist yet", function () { + describe("An entry for the current month is created. So,", function () { + beforeAll(function () { + this.currentPlayers = 33; + + const playerHistory = [ + { + year: "2022", + month: "10", + averagePlayers: 75, + trackedPlayers: [], + }, + ]; + + this.game = { + id: 1, + name: "Test Game", + playerHistory: PlayerHistory.manyFromDbEntry(playerHistory), + }; - it("No new entry is created", function () { - expect(this.result.playerHistory.length).toBe(this.historyLength); - }); + this.result = Game.fromDbEntry(this.game); - it("The existing entry is updated.", function () { - expect(this.result.playerHistory[0]).toBeInstanceOf(PlayerHistory); - expect(this.result.playerHistory[0].trackedPlayers[0].players).toBe( - this.currentPlayers, - ); - }); + this.result.pushCurrentPlayers(this.currentPlayers); }); - }); - describe("When this month's player history entry does not exist yet", function () { - describe("An entry for the current month is created. So,", function () { - beforeAll(function () { - this.currentPlayers = 33; - - const playerHistory = [ - { - year: "2022", - month: "10", - averagePlayers: 75, - trackedPlayers: [], - }, - ]; - - this.game = { - id: 1, - name: "Test Game", - playerHistory: PlayerHistory.manyFromDbEntry(playerHistory), - }; - - this.result = Game.fromDbEntry(this.game); - - this.result.pushCurrentPlayers(this.currentPlayers); - }); - - it("this month's entry is created", function () { - expect(this.result.playerHistory.length).toBe(2); - expect(this.result.playerHistory[1]).toBeInstanceOf(PlayerHistory); - }); - - it("the last time we checked the game was played by 33 players", function () { - expect(this.result.playerHistory[1].trackedPlayers[0].players).toBe( - this.currentPlayers, - ); - }); - - it("the existing entry does not change", function () { - expect(this.game.playerHistory[0]).toEqual(this.result.playerHistory[0]); - }); + it("this month's entry is created", function () { + expect(this.result.playerHistory.length).toBe(2); + expect(this.result.playerHistory[1]).toBeInstanceOf(PlayerHistory); + }); + + it("the last time we checked the game was played by 33 players", function () { + expect(this.result.playerHistory[1].trackedPlayers[0].players).toBe( + this.currentPlayers, + ); + }); + + it("the existing entry does not change", function () { + expect(this.game.playerHistory[0]).toEqual(this.result.playerHistory[0]); }); }); }); + }); - describe(".pushSteamchartsPlayerHistory adds player histories from Steamcharts to existing entries while keeping the order intact.", function () { - beforeAll(function () { - const steamApp = getXSampleSteamApps(1); - - this.releaseDate = ""; - this.developers = []; + describe(".pushSteamchartsPlayerHistory adds player histories from Steamcharts to existing entries while keeping the order intact.", function () { + beforeAll(function () { + const steamApp = getXSampleSteamApps(1); - this.result = Game.fromSteamApp(steamApp, this.releaseDate, this.developers); + this.result = Game.fromSteamcharts(steamApp); - this.result.pushCurrentPlayers(513); + this.result.pushCurrentPlayers(513); - const gameHistories = []; - gameHistories.push(PlayerHistory.fromRawData(5, "April 2020")); - gameHistories.push(PlayerHistory.fromRawData(15, "July 2020")); - gameHistories.push(PlayerHistory.fromRawData(55, "February 2020")); + const gameHistories = []; + gameHistories.push(PlayerHistory.fromRawData(5, "April 2020")); + gameHistories.push(PlayerHistory.fromRawData(15, "July 2020")); + gameHistories.push(PlayerHistory.fromRawData(55, "February 2020")); - this.result.pushSteamchartsPlayerHistory(gameHistories); - }); + this.result.pushSteamchartsPlayerHistory(gameHistories); + }); - it("The result is an instance of Game", function () { - expect(this.result).toBeInstanceOf(Game); - }); + it("The result is an instance of Game", function () { + expect(this.result).toBeInstanceOf(Game); + }); - it("The result has a property release date, which equals an empty string", function () { - expect(this.result.releaseDate).toBe(""); - }); + it("The result has a property release date, which equals an empty string", function () { + expect(this.result.releaseDate).toBe(""); + }); - it("The result has a property developers, which equals an empty array", function () { - expect(this.result.developers).toEqual([]); - }); + it("The result has a property developers, which equals an empty array", function () { + expect(this.result.developers).toEqual([]); + }); - it("The game has three new player history entries", function () { - expect(this.result.playerHistory.length).toBe(4); - }); + it("The game has three new player history entries", function () { + expect(this.result.playerHistory.length).toBe(4); + }); - it("All player history entries are in correct order", function () { - expect(this.result.playerHistory[0].averagePlayers).toBe(55); - expect(this.result.playerHistory[1].averagePlayers).toBe(5); - expect(this.result.playerHistory[2].averagePlayers).toBe(15); - expect(this.result.playerHistory[3].averagePlayers).toBe(513); - }); + it("All player history entries are in correct order", function () { + expect(this.result.playerHistory[0].averagePlayers).toBe(55); + expect(this.result.playerHistory[1].averagePlayers).toBe(5); + expect(this.result.playerHistory[2].averagePlayers).toBe(15); + expect(this.result.playerHistory[3].averagePlayers).toBe(513); + }); - it("The games player history entries are new PlayerHistory class instances", function () { - expect(this.result.playerHistory[0]).toBeInstanceOf(PlayerHistory); - expect(this.result.playerHistory[1]).toBeInstanceOf(PlayerHistory); - expect(this.result.playerHistory[2]).toBeInstanceOf(PlayerHistory); - expect(this.result.playerHistory[3]).toBeInstanceOf(PlayerHistory); - }); + it("The games player history entries are new PlayerHistory class instances", function () { + expect(this.result.playerHistory[0]).toBeInstanceOf(PlayerHistory); + expect(this.result.playerHistory[1]).toBeInstanceOf(PlayerHistory); + expect(this.result.playerHistory[2]).toBeInstanceOf(PlayerHistory); + expect(this.result.playerHistory[3]).toBeInstanceOf(PlayerHistory); }); }); - describe(".updateGameDetails", function () { - describe("When we try to update missing developers,", function () { + describe(".updateGameDetailsFrom", function () { + describe("When we try to update a game with existing developers", function () { beforeAll(function () { - this.developers = ["Valve", "Test Dev"]; - this.game = getXGamesWithoutDetails(1)[0]; - this.game.updateGameDetails(this.developers, [], ""); - }); + this.existingDevelopers = ["Valve", "Hopoo Games"]; + + this.game.developers = this.existingDevelopers; - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); }); - it("the game's developers are updated", function () { - expect(this.game.developers).toEqual(this.developers); + it("the game's developers stays unchanged", function () { + expect(this.game.developers).toBe(this.existingDevelopers); }); }); - describe("When we try to update existing developers,", function () { - beforeAll(function () { - this.game = getOneSteamAppInstantiatedGame(); + describe("When we try to update a game with no existing developers", function () { + describe("and the provided html page doesn't contain a developer section", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - this.game.updateGameDetails(["Test Dev"], [], ""); - }); + this.game.updateGameDetailsFrom(page); + }); - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + it("the game's developers stay unchanged", function () { + expect(this.game.developers).toEqual([]); + }); }); - it("the game's developers stay the same", function () { - expect(this.game.developers).toEqual(["Valve", "Hopoo Games"]); + describe("and the provided html page contains two developers,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(counterStrikeHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); + }); + + it("the game's developers are correctly changed", function () { + expect(this.game.developers).toEqual(["Valve", "Hidden Path Entertainment"]); + }); }); }); - describe("When we try to update the missing genres,", function () { + describe("When we try to update a game with existing genres", function () { beforeAll(function () { - this.genres = ["RPG", "Strategy"]; - this.game = getXGamesWithoutDetails(1)[0]; - this.game.updateGameDetails([], this.genres, ""); - }); + this.existingGenres = ["Action", "RPG"]; - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + this.game.genres = this.existingGenres; + + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); }); - it("the game's genres are updated", function () { - expect(this.game.genres).toEqual(this.genres); + it("the game's genres stays unchanged", function () { + expect(this.game.genres).toBe(this.existingGenres); }); }); - describe("When we try to update existing genres,", function () { - beforeAll(function () { - this.game = getOneSteamAppInstantiatedGame(); + describe("When we try to update a game with no existing genres", function () { + describe("and the provided html page doesn't contain a genres section,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - this.game.updateGameDetails([], ["Test Genre"], ""); - }); + this.game.updateGameDetailsFrom(page); + }); - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + it("the game's genres stay unchanged", function () { + expect(this.game.genres).toEqual([]); + }); }); - it("the game's genres stay the same", function () { - expect(this.game.genres).toEqual(["Action", "Adventure"]); + describe("and the provided html page contains the genres,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); + }); + + it("the game's genres are correctly changed", function () { + expect(this.game.genres).toEqual(["Action", "Indie"]); + }); }); }); - describe("When we try to update the missing description,", function () { + describe("When we try to update a game with an existing description", function () { beforeAll(function () { - this.description = "Test description"; - this.game = getXGamesWithoutDetails(1)[0]; - this.game.updateGameDetails([], [], this.description); - }); + this.existingDescription = "Test description"; + + this.game.description = this.existingDescription; - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); }); - it("the game's description is updated", function () { - expect(this.game.description).toBe(this.description); + it("the game's description stays unchanged", function () { + expect(this.game.description).toBe(this.existingDescription); }); }); - describe("When we try to update an existing description,", function () { - beforeAll(function () { - this.game = getOneSteamAppInstantiatedGame(); + describe("When we try to update a game with no existing description", function () { + describe("and the provided html page doesn't contain a description section,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); - this.game.updateGameDetails([], [], "Test description"); - }); + this.game.updateGameDetailsFrom(page); + }); - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + it("the game's description remains unchanged", function () { + expect(this.game.description).toEqual(""); + }); }); - it("the game's description stays the same", function () { - expect(this.game.description).toEqual("Best game"); + describe("and the provided html page contains the description,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateGameDetailsFrom(page); + }); + + it("the game's description is correctly changed", function () { + expect(this.game.description).toEqual( + "Escape a chaotic alien planet by fighting through hordes of frenzied monsters – with your friends, or on your own. Combine loot in surprising ways and master each character until you become the havoc you feared upon your first crash landing.", + ); + }); }); }); }); describe(".updateReleaseDate", function () { - describe("When we try to update a missing release date,", function () { + describe("When we try to update the date of a game with an existing release date,", function () { beforeAll(function () { - this.releaseDate = new Date("23 July 2023"); - this.game = getXGamesWithoutDetails(1)[0]; + this.existingDate = new Date("23 July 2023"); - this.game.updateReleaseDate(this.releaseDate); - }); + this.game.releaseDate = this.existingDate; - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateReleaseDate(page); }); - it("the game's release date is updated", function () { - expect(this.game.releaseDate).toBe(this.releaseDate); + it("the game's release date stays unchanged", function () { + expect(this.game.releaseDate).toBe(this.existingDate); }); }); - describe("When we try to update an existing release date,", function () { - beforeAll(function () { - this.game = getOneSteamAppInstantiatedGame(); + describe("When we try to use a page that has no existing release date", function () { + describe("and the provided html page doesn't contain a valid release date,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(karmazooHtmlDetailsPageSteamDb); + + this.game.updateReleaseDate(page); + }); - this.game.updateReleaseDate("22 July 2019"); + it("the release date stays unchanged", function () { + expect(this.game.releaseDate).toBe(""); + }); }); - it("a game is returned.", function () { - expect(this.game).toBeInstanceOf(Game); + describe("and the provided html page doesn't contain a date section", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsPageMissingInfo); + + this.game.updateReleaseDate(page); + }); + + it("the release date stays unchanged", function () { + expect(this.game.releaseDate).toBe(""); + }); }); - it("the game's release date stays the same", function () { - expect(this.game.releaseDate).toBe("21 July 2019"); + describe("and the provided html page contains a valid release date,", function () { + beforeAll(function () { + this.game = getXGamesWithoutDetails(1)[0]; + const page = getParsedHtmlPage(riskOfRainHtmlDetailsSteamDb); + + this.game.updateReleaseDate(page); + }); + + it("the release date is changed to the correct value", function () { + expect(this.game.releaseDate).toEqual(new Date("11 August 2020 UTC")); + }); }); }); }); diff --git a/backend/src/core/models/games.aggregate.js b/backend/src/core/models/games.aggregate.js new file mode 100644 index 000000000..9e375a544 --- /dev/null +++ b/backend/src/core/models/games.aggregate.js @@ -0,0 +1,51 @@ +import { Game } from "./game.js"; + +export class GamesAggregate { + #games; + + constructor(games) { + this.#games = games.map((game) => Game.fromDbEntry(game)); + } + + get content() { + return this.#games; + } + + get ids() { + return this.#games.map((game) => game.id); + } + + get isEmpty() { + if (this.#games.length > 0) return false; + + return true; + } + + updateGameDetailsFrom(htmlDetailsPages) { + this.#games = this.#games.map((game) => { + const gameCopy = game.copy(); + + const page = this.findPageForGameById(htmlDetailsPages, gameCopy.id); + + gameCopy.updateGameDetailsFrom(page); + + return gameCopy; + }); + } + + findPageForGameById(htmlDetailsPages, gameId) { + return htmlDetailsPages.find((page) => gameId === page.id).page; + } + + extractReleaseDatesFrom(htmlDetailsPages) { + this.#games = this.#games.map((game) => { + const gameCopy = game.copy(); + + const page = this.findPageForGameById(htmlDetailsPages, gameCopy.id); + + gameCopy.updateReleaseDate(page); + + return gameCopy; + }); + } +} diff --git a/backend/src/core/models/games.aggregate.spec.js b/backend/src/core/models/games.aggregate.spec.js new file mode 100644 index 000000000..c43f4a483 --- /dev/null +++ b/backend/src/core/models/games.aggregate.spec.js @@ -0,0 +1,117 @@ +import { getParsedHtmlPages } from "../../../assets/html.details.pages.mock.js"; +import { counterStrikeHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/counter.strike.html.details.page.js"; +import { riskOfRainHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/risk.of.rain.html.details.page.js"; +import { Game } from "./game.js"; +import { + getXGamesWithoutDetails, + getXsteamchartsInstantiatedGames, +} from "./game.mocks.js"; +import { GamesAggregate } from "./games.aggregate.js"; + +describe("GamesAggregate", function () { + describe(".getIds", () => { + describe("if the aggregate contains two games,", function () { + beforeAll(function () { + const games = new GamesAggregate(getXGamesWithoutDetails(2)); + + this.result = games.ids; + }); + + it("the games' ids are returned.", function () { + expect(this.result).toEqual([1, 2]); + }); + }); + }); + + describe(".isEmpty", function () { + describe("when the aggregate contains no games", function () { + beforeAll(function () { + const gamesArray = new GamesAggregate([]); + + this.result = gamesArray.isEmpty; + }); + + it("the emptyness check is true", function () { + expect(this.result).toBeTruthy(); + }); + }); + + describe("when the aggregate contains games", function () { + beforeAll(function () { + const gamesArray = new GamesAggregate(getXGamesWithoutDetails(2)); + + this.result = gamesArray.isEmpty; + }); + + it("the emptyness check is false", function () { + expect(this.result).toBeFalsy(); + }); + }); + }); + + describe(".updateGameDetailsFrom.", function () { + describe("When we try to update two games with missing details,", function () { + beforeAll(function () { + this.result = new GamesAggregate(getXGamesWithoutDetails(2)); + + const htmlDetailsPages = [ + counterStrikeHtmlDetailsSteamDb, + riskOfRainHtmlDetailsSteamDb, + ]; + + const parsedPages = getParsedHtmlPages(htmlDetailsPages); + + this.result.updateGameDetailsFrom(parsedPages); + }); + + it("the first game's details are updated", function () { + expect(this.result.content[0].developers).toEqual([ + "Valve", + "Hidden Path Entertainment", + ]); + expect(this.result.content[0].genres).toEqual(["Action", "Free to Play"]); + expect(this.result.content[0].description).toBe( + "Counter-Strike: Global Offensive (CS: GO) expands upon the team-based action gameplay that it pioneered when it was launched 19 years ago. CS: GO features new maps, characters, weapons, and game modes, and delivers updated versions of the classic CS content (de_dust2, etc.).", + ); + }); + + it("the second game's details are updated", function () { + expect(this.result.content[1].developers).toEqual(["Hopoo Games"]); + expect(this.result.content[1].genres).toEqual(["Action", "Indie"]); + expect(this.result.content[1].description).toBe( + "Escape a chaotic alien planet by fighting through hordes of frenzied monsters – with your friends, or on your own. Combine loot in surprising ways and master each character until you become the havoc you feared upon your first crash landing.", + ); + }); + }); + }); + + describe(".extractReleaseDatesFrom.", function () { + describe("When we try to update two games with missing release dates,", function () { + beforeAll(function () { + const games = getXsteamchartsInstantiatedGames(2); + this.gamesArray = new GamesAggregate(games); + + const htmlDetailsPages = [ + counterStrikeHtmlDetailsSteamDb, + riskOfRainHtmlDetailsSteamDb, + ]; + + const parsedPages = getParsedHtmlPages(htmlDetailsPages); + + this.gamesArray.extractReleaseDatesFrom(parsedPages); + }); + + it("the first game's release date is updated", function () { + expect(this.gamesArray.content[0].releaseDate).toEqual( + new Date("21 August 2012 UTC"), + ); + }); + + it("the second game's release date is updated", function () { + expect(this.gamesArray.content[1].releaseDate).toEqual( + new Date("11 August 2020 UTC"), + ); + }); + }); + }); +}); diff --git a/backend/src/core/models/player.history.mocks.js b/backend/src/core/models/player.history.mocks.js new file mode 100644 index 000000000..eb600edcf --- /dev/null +++ b/backend/src/core/models/player.history.mocks.js @@ -0,0 +1,15 @@ +import { PlayerHistory } from "./player.history.js"; +import { getXSampleTrackedPlayers } from "./tracked.players.mocks.js"; + +const histories = [ + { + year: "2020", + month: "September", + trackedPlayers: getXSampleTrackedPlayers(2), + averagePlayers: 105, + }, +]; + +export const getSamplePlayerHistory = () => { + return PlayerHistory.manyFromDbEntry(histories); +}; diff --git a/backend/src/core/models/steam.app.js b/backend/src/core/models/steam.app.js index 975d0fec5..ad9106499 100644 --- a/backend/src/core/models/steam.app.js +++ b/backend/src/core/models/steam.app.js @@ -12,6 +12,14 @@ export class SteamApp { "unknown", ]); + static #createValidTypesEnum(values) { + const enumObject = {}; + for (const val of values) { + enumObject[val] = val; + } + return Object.freeze(enumObject); + } + copy() { const copy = new SteamApp(); copy.appid = this.appid; @@ -23,46 +31,6 @@ export class SteamApp { return copy; } - triedViaSteamWeb() { - this.triedVia.push(ValidDataSources.validDataSources.steamWeb); - } - - triedViaSteamchartsWeb() { - this.triedVia.push(ValidDataSources.validDataSources.steamcharts); - } - - triedViaSteamDb() { - this.triedVia.push(ValidDataSources.validDataSources.steamDb); - } - - failedViaSteamWeb() { - this.failedVia.push(ValidDataSources.validDataSources.steamWeb); - } - - failedViaSteamchartsWeb() { - this.failedVia.push(ValidDataSources.validDataSources.steamcharts); - } - - failedViaSteamDb() { - this.failedVia.push(ValidDataSources.validDataSources.steamDb); - } - - isGame() { - return this.type === SteamApp.validTypes.game; - } - - set appType(type) { - this.type = type; - } - - static #createValidTypesEnum(values) { - const enumObject = {}; - for (const val of values) { - enumObject[val] = val; - } - return Object.freeze(enumObject); - } - static manyFromSteamApi(apps) { return apps.map((app) => SteamApp.oneFromSteamApi(app)); } @@ -98,4 +66,62 @@ export class SteamApp { return steamAppsSource.filter((app) => !targetAppIds.includes(app.appid)); } + + get isGame() { + return this.type === SteamApp.validTypes.game; + } + + recordHtmlAttempt(page, source) { + this.#addHtmlPageTriedViaSource(source); + + if (page.toString() === "") this.#addHtmlPageFailedViaSource(source); + } + + #addHtmlPageTriedViaSource(source) { + if (this.triedVia.includes(source)) return; + + this.triedVia.push(source); + } + + #addHtmlPageFailedViaSource(source) { + if (this.failedVia.includes(source)) return; + + this.failedVia.push(source); + } + + updateSteamAppType(page, source) { + this.type = this.#getType(page, source); + } + + #getType(page, source) { + if (source === ValidDataSources.validDataSources.steamWeb) + return this.#getSteamWebAppType(page); + if (source === ValidDataSources.validDataSources.steamcharts) + return this.#getSteamchartsAppType(page); + } + + #getSteamWebAppType(page) { + const breadcrumbElement = page.querySelector(".blockbg"); + + if (!breadcrumbElement) return SteamApp.validTypes.unknown; + + const breadcrumbText = breadcrumbElement.children[0].textContent; + + if (breadcrumbText !== "All Software" && breadcrumbText !== "All Games") + return SteamApp.validTypes.unknown; + + for (let child of breadcrumbElement.children) { + if (child.textContent === "Downloadable Content") + return SteamApp.validTypes.downloadableContent; + } + + return SteamApp.validTypes.game; + } + + // TODO https://github.com/lukatarman/steam-game-stats/issues/178 + #getSteamchartsAppType(page) { + if (page.toString() === "") return SteamApp.validTypes.unknown; + + return SteamApp.validTypes.game; + } } diff --git a/backend/src/core/models/steam.app.mocks.js b/backend/src/core/models/steam.app.mocks.js index 902395b74..3b33b491b 100644 --- a/backend/src/core/models/steam.app.mocks.js +++ b/backend/src/core/models/steam.app.mocks.js @@ -1,5 +1,4 @@ import { SteamApp } from "./steam.app.js"; -import { ValidDataSources } from "./valid.data.sources.js"; export const getXSampleSteamApps = (amount) => { return Array.from({ length: amount }, (x, index) => @@ -7,143 +6,32 @@ export const getXSampleSteamApps = (amount) => { ); }; -export const getThreeSteamwebUntriedFilteredSteamApps = () => { - return [ - { - appid: 1, - name: "Risk of Strain", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 2, - name: "Risk of Stain", - type: "unknown", - triedVia: [], - failedVia: [], - }, - { - appid: 3, - name: "Risk of Sprain", - type: "game", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 4, - name: "Risk of Rain", - type: "unknown", - triedVia: [ - ValidDataSources.validDataSources.steamcharts, - ValidDataSources.validDataSources.steamWeb, - ], - failedVia: [ValidDataSources.validDataSources.steamWeb], - }, - { - appid: 5, - name: "Risk of Train Soundtrack", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 6, - name: "Risk of Cane DLC", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 7, - name: "Risk of Plain Demo", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 8, - name: "Risk of Crane", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - ]; +export const getThreeSourceUntriedFilteredSteamApps = (amount, source) => { + return Array.from({ length: amount }, (x, index) => + SteamApp.oneFromSteamApi({ appid: index + 1, name: `Game #${index + 1}` }), + ).map((app, index) => { + if (isSecondFourthOrSeventhValue(index)) return app; + + app.triedVia.push(source); + return app; + }); }; -export const getThreeSteamchartsUntriedFilteredSteamApps = () => { - return [ - { - appid: 1, - name: "Risk of Strain", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 2, - name: "Risk of Stain", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 3, - name: "Risk of Gain", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [ValidDataSources.validDataSources.steamWeb], - }, - { - appid: 4, - name: "Risk of Sprain", - type: "game", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 5, - name: "Risk of Rain", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamcharts], - failedVia: [], - }, - { - appid: 6, - name: "Risk of Train", - type: "unknown", + +export const getXSampleSteamAppsMarkedAsGames = (amount) => { + return Array.from({ length: amount }, (x, index) => + SteamApp.oneFromDbEntry({ + appid: index + 1, + name: `Game #${index + 1}`, + type: SteamApp.validTypes.game, triedVia: [], failedVia: [], - }, - { - appid: 7, - name: "Risk of Cane Soundtrack", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 8, - name: "Risk of Plain DLC", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 9, - name: "Risk of Crane Demo", - type: "unknown", - triedVia: [ValidDataSources.validDataSources.steamWeb], - failedVia: [], - }, - { - appid: 10, - name: "Risk of Lane", - type: "unknown", - triedVia: [ - ValidDataSources.validDataSources.steamWeb, - ValidDataSources.validDataSources.steamcharts, - ], - failedVia: [ValidDataSources.validDataSources.steamWeb], - }, - ]; + }), + ); +}; + +const isSecondFourthOrSeventhValue = (index) => { + if (index === 1) return true; + if (index === 3) return true; + if (index === 6) return true; + return false; }; diff --git a/backend/src/core/models/steam.app.spec.js b/backend/src/core/models/steam.app.spec.js index da979d327..39cf59412 100644 --- a/backend/src/core/models/steam.app.spec.js +++ b/backend/src/core/models/steam.app.spec.js @@ -2,6 +2,12 @@ import { gamesMock } from "../../../assets/small.data.set.js"; import { smallestGamesMock } from "../../../assets/smallest.data.set.js"; import { ValidDataSources } from "./valid.data.sources.js"; import { SteamApp } from "./steam.app.js"; +import { getXSampleSteamApps } from "./steam.app.mocks.js"; +import { feartressGameHtmlDetailsPage } from "../../../assets/steam-details-pages/feartress.game.html.details.page.js"; +import { gta5ageRestrictedHtmlDetailsPage } from "../../../assets/steam-details-pages/gta.5.age.restricted.html.details.page.js"; +import { padakVideoHtmlDetailsPage } from "../../../assets/steam-details-pages/padak.video.html.details.page.js"; +import { theSims4dlcHtmlDetailsPage } from "../../../assets/steam-details-pages/the.sims.4.dlc.html.details.page.js"; +import { getParsedHtmlPage } from "../../../assets/html.details.pages.mock.js"; describe("SteamApp", function () { describe(".copy", function () { @@ -35,179 +41,6 @@ describe("SteamApp", function () { }); }); - describe(".triedViaSteamWeb", function () { - describe("pushes 'steamWeb' into the triedVia property. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.triedViaSteamWeb(); - }); - - it("the triedVia property array value is 'steamWeb'", function () { - expect(this.result.triedVia[0]).toBe(ValidDataSources.validDataSources.steamWeb); - }); - }); - }); - - describe(".triedViaSteamchartsWeb", function () { - describe("pushes 'steamCharts' into the triedVia property. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.triedViaSteamchartsWeb(); - }); - - it("the triedVia property array value is 'steamCharts'", function () { - expect(this.result.triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - }); - }); - }); - - describe(".triedViaSteamDb", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.triedViaSteamDb(); - }); - - it("the resulting property is 'steamDb'", function () { - expect(this.result.triedVia[0]).toBe(ValidDataSources.validDataSources.steamDb); - }); - }); - - describe(".failedViaSteamWeb", function () { - describe("pushes 'steamWeb' into the filedVia property. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.failedViaSteamWeb(); - }); - - it("the triedVia property array value is 'steamWeb'", function () { - expect(this.result.failedVia[0]).toBe(ValidDataSources.validDataSources.steamWeb); - }); - }); - }); - - describe(".failedViaSteamcharts", function () { - describe("pushes 'steamcharts' into the filedVia property. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.failedViaSteamchartsWeb(); - }); - - it("the triedVia property array value is 'steamcharts'", function () { - expect(this.result.failedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - }); - }); - }); - - describe(".failedViaSteamDb", function () { - describe("pushes 'steamDb' into the filedVia property. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.failedViaSteamDb(); - }); - - it("the triedVia property array value is 'steamDb'", function () { - expect(this.result.failedVia[0]).toBe(ValidDataSources.validDataSources.steamDb); - }); - }); - }); - - describe(".isGame", function () { - describe("checks if the type property of the class instance equals 'games'. So, ", function () { - describe("if an app's type property equals 'game'", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.steamApp = SteamApp.oneFromSteamApi(this.app); - this.steamApp.type = SteamApp.validTypes.game; - }); - - it("the method returns true", function () { - expect(this.steamApp.isGame()).toBeTrue(); - }); - }); - - describe("if an app's type property does not equal 'game'", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.steamApp = SteamApp.oneFromSteamApi(this.app); - this.steamApp.type = SteamApp.validTypes.unknown; - }); - - it("the method returns false", function () { - expect(this.steamApp.isGame()).toBeFalse(); - }); - }); - }); - }); - - describe(".appType", function () { - describe("sets the 'type' property to whatever was passed in as an argument. When this is done,", function () { - beforeAll(function () { - this.app = { - name: "Castlevania", - appid: 1, - }; - - this.type = SteamApp.validTypes.game; - - this.result = SteamApp.oneFromSteamApi(this.app); - - this.result.appType = this.type; - }); - - it("the 'type' property equals 'game'", function () { - expect(this.result.type).toBe(SteamApp.validTypes.game); - }); - }); - }); - describe(".manyFromSteamApi returns an array of SteamApp instances.", function () { describe("When two apps are passed into it,", function () { beforeAll(function () { @@ -412,4 +245,174 @@ describe("SteamApp", function () { }); }); }); + + describe(".recordHtmlAttempt", () => { + describe("if the html page is not empty", function () { + describe("if the steamApp's triedVia doesn't already include the provided source", function () { + beforeAll(function () { + this.source = ValidDataSources.validDataSources.steamWeb; + + this.steamApp = getXSampleSteamApps(1)[0]; + + const page = feartressGameHtmlDetailsPage; + + this.steamApp.recordHtmlAttempt(page, this.source); + }); + + it("the steam app is mark as tried via steam web", function () { + expect(this.steamApp.triedVia).toEqual([this.source]); + }); + }); + + describe("if the steamApp's triedVia already includes the provided source, and we try to add another", function () { + beforeAll(function () { + this.source = ValidDataSources.validDataSources.steamWeb; + + this.steamApp = getXSampleSteamApps(1)[0]; + + const page = feartressGameHtmlDetailsPage; + + this.steamApp.recordHtmlAttempt(page, this.source); + this.steamApp.recordHtmlAttempt(page, this.source); + }); + + it("the steam app is marked as tried via steam web only once", function () { + expect(this.steamApp.triedVia).toEqual([this.source]); + }); + }); + }); + + describe("if the html page is empty", function () { + describe("if the steamApp's failedVia doesn't already include steamWeb", function () { + beforeAll(function () { + this.source = ValidDataSources.validDataSources.steamWeb; + + this.steamApp = getXSampleSteamApps(1)[0]; + + const page = ""; + + this.steamApp.recordHtmlAttempt(page, this.source); + }); + + it("the steam app is mark as tried via steam web", function () { + expect(this.steamApp.triedVia).toEqual([this.source]); + }); + + it("the steam app is marked as failed via steam web", function () { + expect(this.steamApp.failedVia).toEqual([this.source]); + }); + }); + + describe("if the steamApp's failedVia already includes steamWeb and we try to add it again", function () { + beforeAll(function () { + this.source = ValidDataSources.validDataSources.steamWeb; + + this.steamApp = getXSampleSteamApps(1)[0]; + + const page = ""; + + this.steamApp.recordHtmlAttempt(page, this.source); + this.steamApp.recordHtmlAttempt(page, this.source); + }); + + it("the steam app is mark as tried via steam web", function () { + expect(this.steamApp.triedVia).toEqual([this.source]); + }); + + it("the steam app is marked as failed via steam web only once", function () { + expect(this.steamApp.failedVia).toEqual([this.source]); + }); + }); + }); + }); + + describe(".updateSteamAppType", function () { + describe("via steam web", function () { + describe("when the app is age restricted", function () { + beforeAll(function () { + this.app = getXSampleSteamApps(1)[0]; + + const source = ValidDataSources.validDataSources.steamWeb; + const page = getParsedHtmlPage(gta5ageRestrictedHtmlDetailsPage); + + this.app.updateSteamAppType(page, source); + }); + + it("the app will be marked as unknown", function () { + expect(this.app.type).toEqual(SteamApp.validTypes.unknown); + }); + }); + + describe("when the app does not have the appropriate text in the first breadcrumb", function () { + beforeAll(function () { + this.app = getXSampleSteamApps(1)[0]; + + const source = ValidDataSources.validDataSources.steamWeb; + const page = getParsedHtmlPage(padakVideoHtmlDetailsPage); + + this.app.updateSteamAppType(page, source); + }); + + it("the app will be marked as unknown", function () { + expect(this.app.type).toEqual(SteamApp.validTypes.unknown); + }); + }); + + describe("when app has 'downloadable content' in its breadcrumb", function () { + beforeAll(function () { + this.app = getXSampleSteamApps(1)[0]; + + const source = ValidDataSources.validDataSources.steamWeb; + const page = getParsedHtmlPage(theSims4dlcHtmlDetailsPage); + + this.app.updateSteamAppType(page, source); + }); + + it("the app will be marked as 'Downloadable Content'", function () { + expect(this.app.type).toEqual(SteamApp.validTypes.downloadableContent); + }); + }); + + describe("when the app is a game", function () { + beforeAll(function () { + this.app = getXSampleSteamApps(1)[0]; + + const source = ValidDataSources.validDataSources.steamWeb; + const page = getParsedHtmlPage(feartressGameHtmlDetailsPage); + + this.app.updateSteamAppType(page, source); + }); + + it("the app will be marked as a game", function () { + expect(this.app.type).toEqual(SteamApp.validTypes.game); + }); + }); + }); + + describe("via steamcharts", function () { + describe("the provided html page is empty", function () { + beforeAll(async function () { + this.app = getXSampleSteamApps(1)[0]; + const source = ValidDataSources.validDataSources.steamcharts; + this.app.updateSteamAppType("", source); + }); + + it("the app will be marked as unknown", function () { + expect(this.app.type).toBe(SteamApp.validTypes.unknown); + }); + }); + + describe("the provided html page is not empty", function () { + beforeAll(async function () { + this.app = getXSampleSteamApps(1)[0]; + const source = ValidDataSources.validDataSources.steamcharts; + this.app.updateSteamAppType(feartressGameHtmlDetailsPage, source); + }); + + it("the app will be marked as game", function () { + expect(this.app.type).toBe(SteamApp.validTypes.game); + }); + }); + }); + }); }); diff --git a/backend/src/core/models/steam.apps.aggregate.js b/backend/src/core/models/steam.apps.aggregate.js new file mode 100644 index 000000000..59e70320d --- /dev/null +++ b/backend/src/core/models/steam.apps.aggregate.js @@ -0,0 +1,64 @@ +import { Game } from "./game.js"; +import { SteamApp } from "./steam.app.js"; + +export class SteamAppsAggregate { + #apps; + + constructor(steamApps) { + this.#apps = steamApps.map((app) => SteamApp.oneFromDbEntry(app)); + } + + get content() { + return this.#apps; + } + + get isEmpty() { + if (this.#apps.length > 0) return false; + + return true; + } + + identifyTypes(htmlDetailsPages, source) { + this.#apps = this.#apps.map((app) => { + const appCopy = app.copy(); + + const page = this.#findSteamAppHtmlDetailsPage(htmlDetailsPages, appCopy.appid); + + appCopy.recordHtmlAttempt(page, source); + + appCopy.updateSteamAppType(page, source); + + return appCopy; + }); + } + + #findSteamAppHtmlDetailsPage(htmlDetailsPages, steamAppId) { + return htmlDetailsPages.find((page) => steamAppId === page.id).page; + } + + extractGames(htmlDetailsPages) { + return this.#apps + .map((app) => { + if (!app.isGame) return ""; + + const page = this.#findSteamAppHtmlDetailsPage(htmlDetailsPages, app.appid); + + return Game.fromSteamApp(app, page); + }) + .filter((game) => !!game); + } + + recordAttemptsViaSource(htmlDetailsPages, source) { + this.#apps = this.#apps.map((app) => { + const appCopy = app.copy(); + const currentPage = this.#findSteamAppHtmlDetailsPage( + htmlDetailsPages, + appCopy.appid, + ); + + appCopy.recordHtmlAttempt(currentPage, source); + + return appCopy; + }); + } +} diff --git a/backend/src/core/models/steam.apps.aggregate.spec.js b/backend/src/core/models/steam.apps.aggregate.spec.js new file mode 100644 index 000000000..35f760cc9 --- /dev/null +++ b/backend/src/core/models/steam.apps.aggregate.spec.js @@ -0,0 +1,114 @@ +import { getParsedHtmlPages } from "../../../assets/html.details.pages.mock.js"; +import { feartressGameHtmlDetailsPage } from "../../../assets/steam-details-pages/feartress.game.html.details.page.js"; +import { mortalDarknessGameHtmlDetailsPage } from "../../../assets/steam-details-pages/mortal.darkness.game.html.details.page.js"; +import { SteamApp } from "./steam.app.js"; +import { + getXSampleSteamApps, + getXSampleSteamAppsMarkedAsGames, +} from "./steam.app.mocks.js"; +import { SteamAppsAggregate } from "./steam.apps.aggregate.js"; +import { ValidDataSources } from "./valid.data.sources.js"; + +describe("SteamAppsAggregate", function () { + describe(".isEmpty", function () { + describe("when the aggregate contains no steam apps", function () { + beforeAll(function () { + const steamAppsArray = new SteamAppsAggregate([]); + + this.result = steamAppsArray.isEmpty; + }); + + it("the emptyness check returns true", function () { + expect(this.result).toBeTruthy(); + }); + }); + + describe("when the aggregate contains steam apps", function () { + beforeAll(function () { + const steamAppsArray = new SteamAppsAggregate(getXSampleSteamApps(2)); + + this.result = steamAppsArray.isEmpty; + }); + + it("the emptyness check returns false", function () { + expect(this.result).toBeFalsy(); + }); + }); + }); + + describe(".identifyTypes", function () { + describe("When we try to identify two steam apps", function () { + beforeAll(function () { + this.steamAppsArray = new SteamAppsAggregate(getXSampleSteamApps(2)); + + const source = ValidDataSources.validDataSources.steamWeb; + const pages = getParsedHtmlPages(["", mortalDarknessGameHtmlDetailsPage]); + + this.steamAppsArray.identifyTypes(pages, source); + }); + + it("the first app is correctly identified", function () { + expect(this.steamAppsArray.content[0].appid).toBe(1); + expect(this.steamAppsArray.content[0].triedVia).toEqual(["steamWeb"]); + expect(this.steamAppsArray.content[0].failedVia).toEqual(["steamWeb"]); + expect(this.steamAppsArray.content[0].type).toBe("unknown"); + }); + + it("the second app is correctly identified", function () { + expect(this.steamAppsArray.content[1].appid).toBe(2); + expect(this.steamAppsArray.content[1].triedVia).toEqual(["steamWeb"]); + expect(this.steamAppsArray.content[1].failedVia).toEqual([]); + expect(this.steamAppsArray.content[1].type).toBe("game"); + }); + }); + }); + + describe(".extractGames", function () { + describe("when no steam apps are marked as games", function () { + beforeAll(function () { + const steamAppsArray = new SteamAppsAggregate(getXSampleSteamApps(2)); + const pages = getParsedHtmlPages(["", ""]); + + this.result = steamAppsArray.extractGames(pages); + }); + + it("no games are returned", function () { + expect(this.result).toEqual([]); + }); + }); + + describe("when one out of two steam apps is marked as a game", function () { + beforeAll(function () { + const steamAppsArray = new SteamAppsAggregate(getXSampleSteamApps(2)); + const source = ValidDataSources.validDataSources.steamWeb; + const pages = getParsedHtmlPages(["", mortalDarknessGameHtmlDetailsPage]); + + steamAppsArray.identifyTypes(pages, source); + + this.result = steamAppsArray.extractGames(pages); + }); + + it("one game is returned", function () { + expect(this.result.length).toBe(1); + }); + }); + + describe("when two out of two steam apps are marked as games", function () { + beforeAll(function () { + const steamAppsArray = new SteamAppsAggregate( + getXSampleSteamAppsMarkedAsGames(2), + ); + const pages = getParsedHtmlPages([ + feartressGameHtmlDetailsPage, + mortalDarknessGameHtmlDetailsPage, + ]); + + this.result = steamAppsArray.extractGames(pages); + }); + + it("two games are returned", function () { + expect(this.result.length).toBe(2); + }); + }); + }); +}); diff --git a/backend/src/core/models/tracked.players.mocks.js b/backend/src/core/models/tracked.players.mocks.js new file mode 100644 index 000000000..b8a173db1 --- /dev/null +++ b/backend/src/core/models/tracked.players.mocks.js @@ -0,0 +1,11 @@ +import { TrackedPlayers } from "./tracked.players.js"; + +export const getXSampleTrackedPlayers = (amount) => { + const samplePlayerAmount = amount * 12; + const sampleDateTimestamp = 1598911200000 + amount * 86400000; + + return Array.from( + { length: amount }, + (x, index) => new TrackedPlayers(samplePlayerAmount, sampleDateTimestamp), + ); +}; diff --git a/backend/src/core/repositories/games.repository.js b/backend/src/core/repositories/games.repository.js index 288515e85..8883014e8 100644 --- a/backend/src/core/repositories/games.repository.js +++ b/backend/src/core/repositories/games.repository.js @@ -1,5 +1,6 @@ import { Game } from "../models/game.js"; import { daysToMs } from "../../common/time.utils.js"; +import { GamesAggregate } from "../models/games.aggregate.js"; export class GamesRepository { #dbClient; @@ -21,26 +22,26 @@ export class GamesRepository { } async getGamesWithoutDetails(amount) { - return ( - await this.#dbClient - .get("games") - .aggregate([ - { - $match: { - $or: [ - { developers: { $eq: [] } }, - { genres: { $eq: [] } }, - { description: { $eq: "" } }, - ], - }, + const response = await this.#dbClient + .get("games") + .aggregate([ + { + $match: { + $or: [ + { developers: { $eq: [] } }, + { genres: { $eq: [] } }, + { description: { $eq: "" } }, + ], }, - { $limit: amount }, - ]) - .toArray() - ).map((dbEntry) => Game.fromDbEntry(dbEntry)); + }, + { $limit: amount }, + ]) + .toArray(); + + return new GamesAggregate(response); } - async updateGameDetails(games) { + async updateGameDetailsFrom(games) { await Promise.all( games.map((game) => this.#dbClient.updateOne( @@ -59,19 +60,19 @@ export class GamesRepository { } async getGamesWithoutReleaseDates(amount) { - return ( - await this.#dbClient - .get("games") - .aggregate([ - { - $match: { - $or: [{ releaseDate: { $eq: "" } }, { releaseDate: { $gt: new Date() } }], - }, + const response = await this.#dbClient + .get("games") + .aggregate([ + { + $match: { + $or: [{ releaseDate: { $eq: "" } }, { releaseDate: { $gt: new Date() } }], }, - { $limit: amount }, - ]) - .toArray() - ).map((dbEntry) => Game.fromDbEntry(dbEntry)); + }, + { $limit: amount }, + ]) + .toArray(); + + return new GamesAggregate(response); } async updateReleaseDates(games) { diff --git a/backend/src/core/repositories/games.repository.spec.js b/backend/src/core/repositories/games.repository.spec.js index 85e6a13dd..a34e0e446 100644 --- a/backend/src/core/repositories/games.repository.spec.js +++ b/backend/src/core/repositories/games.repository.spec.js @@ -9,6 +9,7 @@ import { getTrendingGamesMockData, } from "../models/game.mocks.js"; import { Game } from "../models/game.js"; +import { GamesAggregate } from "../models/games.aggregate.js"; describe("GamesRepository", function () { describe(".insertManyGames inserts multiple games into the collection.", function () { @@ -141,34 +142,38 @@ describe("GamesRepository", function () { this.databaseClient.disconnect(); }); + it("the result is an instance of GamesAggregate", function () { + expect(this.result).toBeInstanceOf(GamesAggregate); + }); + it("three games are returned", function () { - expect(this.result.length).toBe(3); + expect(this.result.content.length).toBe(3); }); it("the games are an instance of Game", function () { - expect(this.result[0]).toBeInstanceOf(Game); - expect(this.result[1]).toBeInstanceOf(Game); - expect(this.result[2]).toBeInstanceOf(Game); + expect(this.result.content[0]).toBeInstanceOf(Game); + expect(this.result.content[1]).toBeInstanceOf(Game); + expect(this.result.content[2]).toBeInstanceOf(Game); }); it("the first game is missing the developers", function () { - expect(this.result[0].id).toBe(232090); - expect(this.result[0].developers).toEqual([]); + expect(this.result.content[0].id).toBe(232090); + expect(this.result.content[0].developers).toEqual([]); }); it("the second game is missing the genres", function () { - expect(this.result[1].id).toBe(881100); - expect(this.result[1].genres).toEqual([]); + expect(this.result.content[1].id).toBe(881100); + expect(this.result.content[1].genres).toEqual([]); }); it("the third game is missing the description", function () { - expect(this.result[2].id).toBe(620); - expect(this.result[2].description).toEqual(""); + expect(this.result.content[2].id).toBe(620); + expect(this.result.content[2].description).toEqual(""); }); }); }); - describe(".updateGameDetails", function () { + describe(".updateGameDetailsFrom", function () { describe("When the details of 1 game are to be updated,", function () { beforeAll(async function () { this.databaseClient = await initiateInMemoryDatabase(["games"]); @@ -179,7 +184,7 @@ describe("GamesRepository", function () { this.games = getOneGameWithDetails(); - await gamesRepo.updateGameDetails(this.games); + await gamesRepo.updateGameDetailsFrom(this.games); this.result = await gamesRepo.getOneGameById(this.games[0].id); }); @@ -213,29 +218,33 @@ describe("GamesRepository", function () { this.databaseClient.disconnect(); }); + it("the result is an instance of GamesAggregate", function () { + expect(this.result).toBeInstanceOf(GamesAggregate); + }); + it("three games are returned", function () { - expect(this.result.length).toBe(3); + expect(this.result.content.length).toBe(3); }); it("the games are an instance of Game", function () { - expect(this.result[0]).toBeInstanceOf(Game); - expect(this.result[1]).toBeInstanceOf(Game); - expect(this.result[2]).toBeInstanceOf(Game); + expect(this.result.content[0]).toBeInstanceOf(Game); + expect(this.result.content[1]).toBeInstanceOf(Game); + expect(this.result.content[2]).toBeInstanceOf(Game); }); it("the first game has the correct date", function () { - expect(this.result[0].id).toBe(227300); - expect(this.result[0].releaseDate).toEqual(new Date("Thu Oct 17 2999")); + expect(this.result.content[0].id).toBe(227300); + expect(this.result.content[0].releaseDate).toEqual(new Date("Thu Oct 17 2999")); }); it("the second game is missing the release date", function () { - expect(this.result[1].id).toBe(2218750); - expect(this.result[1].releaseDate).toBe(""); + expect(this.result.content[1].id).toBe(2218750); + expect(this.result.content[1].releaseDate).toBe(""); }); it("the third game is missing the release date", function () { - expect(this.result[2].id).toBe(239140); - expect(this.result[2].releaseDate).toBe(""); + expect(this.result.content[2].id).toBe(239140); + expect(this.result.content[2].releaseDate).toBe(""); }); }); }); diff --git a/backend/src/core/repositories/steam.apps.repository.js b/backend/src/core/repositories/steam.apps.repository.js index cd848f575..1f8deb57e 100644 --- a/backend/src/core/repositories/steam.apps.repository.js +++ b/backend/src/core/repositories/steam.apps.repository.js @@ -1,5 +1,5 @@ import { SteamApp } from "../models/steam.app.js"; -import { ValidDataSources } from "../models/valid.data.sources.js"; +import { SteamAppsAggregate } from "../models/steam.apps.aggregate.js"; export class SteamAppsRepository { #dbClient; @@ -42,12 +42,13 @@ export class SteamAppsRepository { ); } - async getSteamWebUntriedFilteredSteamApps(amount) { - const response = await this.#steamAppsCollection + async getSourceUntriedFilteredSteamApps(amount, source) { + const response = await this.#dbClient + .get("steam_apps") .find({ $and: [ { type: SteamApp.validTypes.unknown }, - { triedVia: { $ne: ValidDataSources.validDataSources.steamWeb } }, + { triedVia: { $ne: source } }, { name: { $not: { $regex: /soundtrack$/, $options: "i" } } }, { name: { $not: { $regex: /dlc$/, $options: "i" } } }, { name: { $not: { $regex: /demo$/, $options: "i" } } }, @@ -56,35 +57,13 @@ export class SteamAppsRepository { .limit(amount) .toArray(); - return SteamApp.manyFromDbEntries(response); - } - - async getSteamchartsUntriedFilteredSteamApps(amount) { - const response = await this.#steamAppsCollection - .find({ - $and: [ - { type: SteamApp.validTypes.unknown }, - { - $and: [ - { triedVia: { $ne: ValidDataSources.validDataSources.steamcharts } }, - { triedVia: ValidDataSources.validDataSources.steamWeb }, - ], - }, - { name: { $not: { $regex: /soundtrack$/, $options: "i" } } }, - { name: { $not: { $regex: /dlc$/, $options: "i" } } }, - { name: { $not: { $regex: /demo$/, $options: "i" } } }, - ], - }) - .limit(amount) - .toArray(); - - return SteamApp.manyFromDbEntries(response); + return new SteamAppsAggregate(response); } async getSteamAppsById(ids) { const response = await Promise.all(ids.map((id) => this.getSteamAppById(id))); - return SteamApp.manyFromDbEntries(response); + return new SteamAppsAggregate(response); } async getSteamAppById(id) { diff --git a/backend/src/core/repositories/steam.apps.repository.spec.js b/backend/src/core/repositories/steam.apps.repository.spec.js index 4b713d8f5..c8de5442b 100644 --- a/backend/src/core/repositories/steam.apps.repository.spec.js +++ b/backend/src/core/repositories/steam.apps.repository.spec.js @@ -1,13 +1,13 @@ import { getXGamesWithoutDetails } from "../models/game.mocks.js"; import { SteamApp } from "../models/steam.app.js"; import { - getThreeSteamchartsUntriedFilteredSteamApps, - getThreeSteamwebUntriedFilteredSteamApps, + getThreeSourceUntriedFilteredSteamApps, getXSampleSteamApps, } from "../models/steam.app.mocks.js"; import { ValidDataSources } from "../models/valid.data.sources.js"; import { initiateInMemoryDatabase } from "../../adapters/driven/db/in.memory.database.client.js"; import { SteamAppsRepository } from "./steam.apps.repository.js"; +import { SteamAppsAggregate } from "../models/steam.apps.aggregate.js"; describe("SteamAppsRepository", function () { describe(".insertManyIfNotExist inserts all the non existing steam apps from a given array", function () { @@ -211,366 +211,240 @@ describe("SteamAppsRepository", function () { }); }); - describe(".getSteamWebUntriedFilteredSteamApps.", function () { - describe("If three steam apps out of eigth match the filters, and the amount of 2 is provided", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamwebUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamWebUntriedFilteredSteamApps(2); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the result has two games", function () { - expect(this.result.length).toBe(2); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia).toEqual([]); - expect(this.result[1].failedVia).toEqual([]); + describe(".getSourceUntriedFilteredSteamApps.", function () { + describe("via SteamWeb source", function () { + describe("If three steam apps out of eigth match the filters,", function () { + describe("and a limit of 2 is provided", function () { + beforeAll(async function () { + this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); + + const source = ValidDataSources.validDataSources.steamWeb; + + await insertManyApps( + this.databaseClient, + getThreeSourceUntriedFilteredSteamApps(8, source), + ); + + const steamAppsRepo = new SteamAppsRepository(this.databaseClient); + + this.result = await steamAppsRepo.getSourceUntriedFilteredSteamApps( + 2, + source, + ); + }); + + afterAll(function () { + this.databaseClient.disconnect(); + }); + + it("the result is an instance of SteamAppsAggregate", function () { + expect(this.result).toBeInstanceOf(SteamAppsAggregate); + }); + + it("the result contains two steam apps", function () { + expect(this.result.content.length).toBe(2); + }); + + it("the first steam app is an instance of SteamApp", function () { + expect(this.result.content[0]).toBeInstanceOf(SteamApp); + }); + + it("the first steam app has the correct values", function () { + expect(this.result.content[0].appid).toBe(2); + expect(this.result.content[0].type).toBe("unknown"); + expect(this.result.content[0].triedVia).toEqual([]); + }); + + it("the second steam app is an instance of SteamApp", function () { + expect(this.result.content[1]).toBeInstanceOf(SteamApp); + }); + + it("the second steam app has the correct values", function () { + expect(this.result.content[1].appid).toBe(4); + expect(this.result.content[1].type).toBe("unknown"); + expect(this.result.content[1].triedVia).toEqual([]); + }); + }); + + describe("and the amount of 0 is provided", function () { + beforeAll(async function () { + this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); + + const source = ValidDataSources.validDataSources.steamWeb; + + await insertManyApps( + this.databaseClient, + getThreeSourceUntriedFilteredSteamApps(8, source), + ); + + const steamAppsRepo = new SteamAppsRepository(this.databaseClient); + + this.result = await steamAppsRepo.getSourceUntriedFilteredSteamApps( + 0, + source, + ); + }); + + afterAll(function () { + this.databaseClient.disconnect(); + }); + + it("the result is an instance of SteamAppsAggregate", function () { + expect(this.result).toBeInstanceOf(SteamAppsAggregate); + }); + + it("the result contains three steam apps", function () { + expect(this.result.content.length).toBe(3); + }); + + it("the first steam app an instance of SteamApp", function () { + expect(this.result.content[0]).toBeInstanceOf(SteamApp); + }); + + it("the first steam app has the correct values", function () { + expect(this.result.content[0].appid).toBe(2); + expect(this.result.content[0].type).toBe("unknown"); + expect(this.result.content[0].triedVia).toEqual([]); + }); + + it("the second steam app is an instance of SteamApp", function () { + expect(this.result.content[1]).toBeInstanceOf(SteamApp); + }); + + it("the second steam app has the correct values", function () { + expect(this.result.content[1].appid).toBe(4); + expect(this.result.content[1].type).toBe("unknown"); + expect(this.result.content[1].triedVia).toEqual([]); + }); + + it("the third steam app is an instance of SteamApp", function () { + expect(this.result.content[2]).toBeInstanceOf(SteamApp); + }); + + it("the third steam app has the correct values", function () { + expect(this.result.content[2].appid).toBe(7); + expect(this.result.content[2].type).toBe("unknown"); + expect(this.result.content[2].triedVia).toEqual([]); + }); + }); }); }); - describe("If three steam apps out of eigth match the filters, and the amount of 4 is provided", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamwebUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamWebUntriedFilteredSteamApps(4); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the resulting array has a length of 3", function () { - expect(this.result.length).toBe(3); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia).toEqual([]); - expect(this.result[1].failedVia).toEqual([]); - }); - - it("the third result is an instance of SteamApp", function () { - expect(this.result[2]).toBeInstanceOf(SteamApp); - }); - - it("the third array has the correct values", function () { - expect(this.result[2].appid).toBe(8); - expect(this.result[2].name).toBe("Risk of Crane"); - expect(this.result[2].type).toBe("unknown"); - expect(this.result[2].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - expect(this.result[2].failedVia).toEqual([]); - }); - }); - - describe("If an amount of '0' is provided,", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamwebUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamWebUntriedFilteredSteamApps(0); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the resulting array has a length of 3", function () { - expect(this.result.length).toBe(3); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia).toEqual([]); - expect(this.result[1].failedVia).toEqual([]); - }); - - it("the third result is an instance of SteamApp", function () { - expect(this.result[2]).toBeInstanceOf(SteamApp); - }); - - it("the third array has the correct values", function () { - expect(this.result[2].appid).toBe(8); - expect(this.result[2].name).toBe("Risk of Crane"); - expect(this.result[2].type).toBe("unknown"); - expect(this.result[2].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamcharts, - ); - expect(this.result[2].failedVia).toEqual([]); - }); - }); - }); - - describe(".getSteamchartsUntriedFilteredSteamApps.", function () { - describe("If three steam apps out of ten match the filters, and the amount of 2 is provided", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamchartsUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamchartsUntriedFilteredSteamApps(2); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the result has two games", function () { - expect(this.result.length).toBe(2); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[1].failedVia).toEqual([]); - }); - }); - - describe("If three steam apps out of ten match the filters, and the amount of 4 is provided", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamchartsUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamchartsUntriedFilteredSteamApps(4); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the resulting array has a length of 3", function () { - expect(this.result.length).toBe(3); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[1].failedVia).toEqual([]); - }); - - it("the third result is an instance of SteamApp", function () { - expect(this.result[2]).toBeInstanceOf(SteamApp); - }); - - it("the third array has the correct values", function () { - expect(this.result[2].appid).toBe(3); - expect(this.result[2].name).toBe("Risk of Gain"); - expect(this.result[2].type).toBe("unknown"); - expect(this.result[2].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[2].failedVia).toEqual([ - ValidDataSources.validDataSources.steamWeb, - ]); - }); - }); - - describe("If '0' amount is provided,", function () { - beforeAll(async function () { - this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); - - await insertManyApps( - this.databaseClient, - getThreeSteamchartsUntriedFilteredSteamApps(), - ); - - const steamAppsRepo = new SteamAppsRepository(this.databaseClient); - - this.result = await steamAppsRepo.getSteamchartsUntriedFilteredSteamApps(0); - }); - - afterAll(function () { - this.databaseClient.disconnect(); - }); - - it("the resulting array has a length of 3", function () { - expect(this.result.length).toBe(3); - }); - - it("the first result is an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - }); - - it("the first array has the correct values", function () { - expect(this.result[0].appid).toBe(1); - expect(this.result[0].name).toBe("Risk of Strain"); - expect(this.result[0].type).toBe("unknown"); - expect(this.result[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second result is an instance of SteamApp", function () { - expect(this.result[1]).toBeInstanceOf(SteamApp); - }); - - it("the second array has the correct values", function () { - expect(this.result[1].appid).toBe(2); - expect(this.result[1].name).toBe("Risk of Stain"); - expect(this.result[1].type).toBe("unknown"); - expect(this.result[1].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[1].failedVia).toEqual([]); - }); - - it("the third result is an instance of SteamApp", function () { - expect(this.result[2]).toBeInstanceOf(SteamApp); - }); - - it("the third array has the correct values", function () { - expect(this.result[2].appid).toBe(3); - expect(this.result[2].name).toBe("Risk of Gain"); - expect(this.result[2].type).toBe("unknown"); - expect(this.result[2].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - expect(this.result[2].failedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); + describe("via Steamcharts source", function () { + describe("If three steam apps out of eigth match the filters,", function () { + describe("and the limit of 2 is provided", function () { + beforeAll(async function () { + this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); + + const source = ValidDataSources.validDataSources.steamcharts; + + await insertManyApps( + this.databaseClient, + getThreeSourceUntriedFilteredSteamApps(8, source), + ); + + const steamAppsRepo = new SteamAppsRepository(this.databaseClient); + + this.result = await steamAppsRepo.getSourceUntriedFilteredSteamApps( + 2, + source, + ); + }); + + afterAll(function () { + this.databaseClient.disconnect(); + }); + + it("the result is an instance of SteamAppsAggregate", function () { + expect(this.result).toBeInstanceOf(SteamAppsAggregate); + }); + + it("the result contains two steam app", function () { + expect(this.result.content.length).toBe(2); + }); + + it("the first steam app is an instance of SteamApp", function () { + expect(this.result.content[0]).toBeInstanceOf(SteamApp); + }); + + it("the first steam apphas the correct values", function () { + expect(this.result.content[0].appid).toBe(2); + expect(this.result.content[0].type).toBe("unknown"); + expect(this.result.content[0].triedVia).toEqual([]); + }); + + it("the second steam app is an instance of SteamApp", function () { + expect(this.result.content[1]).toBeInstanceOf(SteamApp); + }); + + it("the second steam app has the correct values", function () { + expect(this.result.content[1].appid).toBe(4); + expect(this.result.content[1].type).toBe("unknown"); + expect(this.result.content[1].triedVia).toEqual([]); + }); + }); + + describe("and the limit of 0 is provided", function () { + beforeAll(async function () { + this.databaseClient = await initiateInMemoryDatabase(["steam_apps"]); + + const source = ValidDataSources.validDataSources.steamcharts; + + await insertManyApps( + this.databaseClient, + getThreeSourceUntriedFilteredSteamApps(8, source), + ); + + const steamAppsRepo = new SteamAppsRepository(this.databaseClient); + + this.result = await steamAppsRepo.getSourceUntriedFilteredSteamApps( + 0, + source, + ); + }); + + afterAll(function () { + this.databaseClient.disconnect(); + }); + + it("the result is an instance of SteamAppsAggregate", function () { + expect(this.result).toBeInstanceOf(SteamAppsAggregate); + }); + + it("the result contains three steam apps", function () { + expect(this.result.content.length).toBe(3); + }); + + it("the first steam app is an instance of SteamApp", function () { + expect(this.result.content[0]).toBeInstanceOf(SteamApp); + }); + + it("the first steam app has the correct values", function () { + expect(this.result.content[0].appid).toBe(2); + expect(this.result.content[0].type).toBe("unknown"); + expect(this.result.content[0].triedVia).toEqual([]); + }); + + it("the second steam app is an instance of SteamApp", function () { + expect(this.result.content[1]).toBeInstanceOf(SteamApp); + }); + + it("the second steam app has the correct values", function () { + expect(this.result.content[1].appid).toBe(4); + expect(this.result.content[1].type).toBe("unknown"); + expect(this.result.content[1].triedVia).toEqual([]); + }); + + it("the third steam app is an instance of SteamApp", function () { + expect(this.result.content[2]).toBeInstanceOf(SteamApp); + }); + + it("the third steam app has the correct values", function () { + expect(this.result.content[2].appid).toBe(7); + expect(this.result.content[2].type).toBe("unknown"); + expect(this.result.content[2].triedVia).toEqual([]); + }); + }); }); }); }); @@ -593,21 +467,25 @@ describe("SteamAppsRepository", function () { this.databaseClient.disconnect(); }); + it("the result is an instance of SteamAppsAggregate", function () { + expect(this.result).toBeInstanceOf(SteamAppsAggregate); + }); + it("two steam apps are returned", function () { - expect(this.result.length).toBe(2); + expect(this.result.content.length).toBe(2); }); it("the steam apps are an instance of SteamApp", function () { - expect(this.result[0]).toBeInstanceOf(SteamApp); - expect(this.result[1]).toBeInstanceOf(SteamApp); + expect(this.result.content[0]).toBeInstanceOf(SteamApp); + expect(this.result.content[1]).toBeInstanceOf(SteamApp); }); it("the first steam app has the correct values", function () { - expect(this.result[0].appid).toBe(1); + expect(this.result.content[0].appid).toBe(1); }); it("the second steam app has the correct values", function () { - expect(this.result[1].appid).toBe(2); + expect(this.result.content[1].appid).toBe(2); }); }); }); diff --git a/backend/src/core/services/game.service.js b/backend/src/core/services/game.service.js deleted file mode 100644 index a464d0547..000000000 --- a/backend/src/core/services/game.service.js +++ /dev/null @@ -1,195 +0,0 @@ -import { Game } from "../models/game.js"; -import { SteamApp } from "../models/steam.app.js"; -import { parseHTML } from "linkedom"; - -export function getSteamAppType(httpDetailsPage) { - const { document } = parseHTML(httpDetailsPage); - - const breadcrumbElement = document.querySelector(".blockbg"); - - if (!breadcrumbElement) return SteamApp.validTypes.unknown; - - const breadcrumbText = breadcrumbElement.children[0].textContent; - - if (breadcrumbText !== "All Software" && breadcrumbText !== "All Games") - return SteamApp.validTypes.unknown; - - for (let child of breadcrumbElement.children) { - if (child.textContent === "Downloadable Content") - return SteamApp.validTypes.downloadableContent; - } - - return SteamApp.validTypes.game; -} - -export function discoverGamesFromSteamWeb(steamApps, htmlDetailsPages) { - return htmlDetailsPages - .map((page, i) => { - if (getSteamAppType(page) === SteamApp.validTypes.game) { - return Game.fromSteamApp( - steamApps[i], - getReleaseDate(page), - getDevelopers(page), - getGenres(page), - getGameDescription(page), - ); - } - }) - .filter((game) => !!game); -} - -export function getReleaseDate(page) { - const { document } = parseHTML(page); - - const releaseDateElement = document.querySelector(".release_date .date"); - - if (!releaseDateElement) return ""; - - const releaseDate = new Date(`${releaseDateElement.textContent.trim()} UTC`); - - return releaseDate == "Invalid Date" ? "" : releaseDate; -} - -export function getDevelopers(page) { - const { document } = parseHTML(page); - - const developers = document.querySelector(".dev_row #developers_list"); - - if (!developers) return []; - - return Array.from(developers.children).map((developer) => developer.textContent.trim()); -} - -export function getGenres(page) { - const { document } = parseHTML(page); - - const genres = document.querySelector("#genresAndManufacturer span"); - - if (!genres) return []; - - return Array.from(genres.children) - .map((genre) => genre.textContent.trim()) - .filter((genre) => !!genre); -} - -export function getGameDescription(page) { - const { document } = parseHTML(page); - - const description = document.querySelector(".game_description_snippet"); - - if (!description) return ""; - - return description.textContent.trim(); -} - -export function updateTypeSideEffectFree(steamApps, htmlDetailsPages) { - return htmlDetailsPages.map((page, i) => { - const copy = steamApps[i].copy(); - const appType = getSteamAppType(page); - - copy.triedViaSteamWeb(); - if (page === "") copy.failedViaSteamWeb(); - - copy.appType = appType; - - return copy; - }); -} - -export function identifyGames(updatedSteamApps) { - const games = updatedSteamApps - .filter((steamApp) => steamApp.isGame()) - .map((steamApp) => Game.fromSteamcharts(steamApp)); - - return games; -} - -export function assignType(result, steamApp) { - if (result) steamApp.appType = SteamApp.validTypes.game; - return steamApp; -} - -export function recordAttemptsViaSteamDb(steamApps, htmlDetailsPages) { - return steamApps.map((app) => { - const appCopy = app.copy(); - appCopy.triedViaSteamDb(); - - const currentPage = htmlDetailsPages.find((page) => page.id == app.appid); - if (currentPage.page === "") appCopy.failedViaSteamDb(); - - return appCopy; - }); -} - -export function updateMissingDetails(games, htmlDetailsPages) { - htmlDetailsPages.forEach(({ page }, i) => { - games[i].updateGameDetails( - getSteamDbDevelopers(page), - getSteamDbGenres(page), - getSteamDbDescription(page), - ); - }); -} - -export function getSteamDbDevelopers(page) { - const { document } = parseHTML(page); - - const developers = document.querySelector( - "table.table.table-bordered.table-hover.table-responsive-flex tbody tr:nth-child(3) td:last-child", - ); - - if (!developers) return []; - - return Array.from(developers.children).map((developer) => developer.textContent); -} - -export function getSteamDbGenres(page) { - const { document } = parseHTML(page); - - const domTableBody = document.querySelector("#info tbody"); - - if (!domTableBody) return []; - - const genresNodes = Array.from(domTableBody.children).filter( - (tableEntry) => tableEntry.children[0].textContent === "Store Genres", - )[0].children[1].childNodes; - - return Array.from(genresNodes) - .filter((genre) => genre.constructor.name === "Text") - .map((genre) => genre.nodeValue.replace(",", "").trim()); -} - -export function getSteamDbDescription(page) { - const { document } = parseHTML(page); - - const description = document.querySelector(".header-description"); - - if (!description) return ""; - - return description.textContent; -} - -export function updateMissingReleaseDates(games, htmlDetailsPages) { - games.forEach((game, i) => { - game.updateReleaseDate(getSteamDbReleaseDate(htmlDetailsPages[i].page)); - }); -} - -export function getSteamDbReleaseDate(page) { - const { document } = parseHTML(page); - - const releaseDateElement = document.querySelector( - "table.table.table-bordered.table-hover.table-responsive-flex tbody tr:last-child td:last-child", - ); - - if (!releaseDateElement) return ""; - - const releaseDateString = releaseDateElement.textContent; - - const releaseDate = new Date(` - ${releaseDateString.slice(0, releaseDateString.indexOf("–") - 1)} UTC`); - - if (releaseDate == "Invalid Date") return ""; - - return releaseDate; -} diff --git a/backend/src/core/services/game.service.spec.js b/backend/src/core/services/game.service.spec.js deleted file mode 100644 index d9af4fe60..000000000 --- a/backend/src/core/services/game.service.spec.js +++ /dev/null @@ -1,863 +0,0 @@ -import { - getSteamAppType, - discoverGamesFromSteamWeb, - updateTypeSideEffectFree, - identifyGames, - assignType, - getReleaseDate, - getDevelopers, - getGenres, - getGameDescription, - updateMissingDetails, - getSteamDbReleaseDate, - getSteamDbDevelopers, - getSteamDbGenres, - getSteamDbDescription, - updateMissingReleaseDates, - recordAttemptsViaSteamDb, -} from "./game.service.js"; -import { animaddicts2gameHtmlDetailsPage } from "../../../assets/steam-details-pages/animaddicts.2.game.html.details.page.js"; -import { feartressGameHtmlDetailsPage } from "../../../assets/steam-details-pages/feartress.game.html.details.page.js"; -import { glitchhikersSoundtrackHtmlDetailsPage } from "../../../assets/steam-details-pages/glitchhikers.soundtrack.html.details.page.js"; -import { gta5ageRestrictedHtmlDetailsPage } from "../../../assets/steam-details-pages/gta.5.age.restricted.html.details.page.js"; -import { padakVideoHtmlDetailsPage } from "../../../assets/steam-details-pages/padak.video.html.details.page.js"; -import { theSims4dlcHtmlDetailsPage } from "../../../assets/steam-details-pages/the.sims.4.dlc.html.details.page.js"; -import { Game } from "../models/game.js"; -import { SteamApp } from "../models/steam.app.js"; -import { ValidDataSources } from "../models/valid.data.sources.js"; -import { mortalDarknessGameHtmlDetailsPage } from "../../../assets/steam-details-pages/mortal.darkness.game.html.details.page.js"; -import { crusaderKingsDetailsPage } from "../../../assets/steam-details-pages/crusader.kings.multiple.developers.html.details.page.js"; -import { riskOfRainHtmlDetailsPageMissingInfo } from "../../../assets/steam-details-pages/risk.of.rain.missing.additional.info.page.js"; -import { counterStrikeHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/counter.strike.html.details.page.js"; -import { riskOfRainHtmlDetailsSteamDb } from "../../../assets/steamdb-details-pages/risk.of.rain.html.details.page.js"; -import { karmazooHtmlDetailsPageSteamDb } from "../../../assets/steamdb-details-pages/karmazoo.html.details.page.js"; -import { getXsteamchartsInstantiatedGames } from "../models/game.mocks.js"; -import { createHtmlDetailsPages } from "../../../assets/html.details.pages.mock.js"; -import { getXSampleSteamApps } from "../models/steam.app.mocks.js"; - -describe("game.service.js", () => { - describe(".getSteamAppType", () => { - describe("game is age restricted - there is no .blockbg class on the page", () => { - let appType; - - beforeAll(async () => { - appType = getSteamAppType(gta5ageRestrictedHtmlDetailsPage); - }); - - it("the function returns 'unknown'", () => { - expect(appType).toBe(SteamApp.validTypes.unknown); - }); - }); - - describe("if there is no 'All Software' or 'All Games' in the first breadcrumb child text", () => { - let appType; - - beforeAll(async () => { - appType = getSteamAppType(padakVideoHtmlDetailsPage); - }); - - it("the function returns 'unknown'", () => { - expect(appType).toBe(SteamApp.validTypes.unknown); - }); - }); - - describe("if the text 'Downloadable Content' is in one of the breadcrumbs", () => { - let appType; - - beforeAll(async () => { - appType = getSteamAppType(theSims4dlcHtmlDetailsPage); - }); - - it("the function returns 'downloadableContent'", () => { - expect(appType).toBe(SteamApp.validTypes.downloadableContent); - }); - }); - - describe(".blockbg class is on the page, 'All Software' or 'All Games' is in the first breadcrumb and there is no 'Downloadable Content' text in the breadcrumbs", () => { - let appType; - - beforeAll(async () => { - appType = getSteamAppType(feartressGameHtmlDetailsPage); - }); - - it("the function returns 'game'", () => { - expect(appType).toBe(SteamApp.validTypes.game); - }); - }); - }); - - describe(".discoverGamesFromSteamWeb", function () { - describe("discovers one game out of a batch of one stemApp, so", function () { - beforeAll(function () { - this.steamApps = [ - { - appid: 1, - name: "Animaddicts", - }, - ]; - this.htmlDetailsPages = [animaddicts2gameHtmlDetailsPage]; - - this.games = discoverGamesFromSteamWeb(this.steamApps, this.htmlDetailsPages); - }); - - it("the length of games is 1", function () { - expect(this.games.length).toBe(1); - }); - - it("the name of the first game array entry is 'Animaddicts'", function () { - expect(this.games[0].name).toBe("Animaddicts"); - }); - - it("the first entry in the games array is an instance of game", function () { - expect(this.games[0]).toBeInstanceOf(Game); - }); - }); - - describe("discovers one game out of a batch of two steamApps, so", function () { - beforeAll(function () { - this.steamApps = [ - { - appid: 1, - name: "Animaddicts", - }, - { - appid: 2, - name: "Glitchhikers Soundtrack 2", - }, - ]; - - this.htmlDetailsPages = [ - animaddicts2gameHtmlDetailsPage, - glitchhikersSoundtrackHtmlDetailsPage, - ]; - - this.games = discoverGamesFromSteamWeb(this.steamApps, this.htmlDetailsPages); - }); - - it("the length of games is 1", function () { - expect(this.games.length).toBe(1); - }); - - it("the name of the first game array entry is 'Animaddicts'", function () { - expect(this.games[0].name).toBe("Animaddicts"); - }); - - it("the first entry in the games array is an instance of game", function () { - expect(this.games[0]).toBeInstanceOf(Game); - }); - }); - - describe("discovers two games out of a batch of three steamApps, so", function () { - beforeAll(function () { - this.steamApps = [ - { - appid: 1, - name: "Animaddicts", - }, - { - appid: 2, - name: "Glitchhikers Soundtrack 2", - }, - { - appid: 3, - name: "Feartress", - }, - ]; - - this.htmlDetailsPages = [ - animaddicts2gameHtmlDetailsPage, - glitchhikersSoundtrackHtmlDetailsPage, - feartressGameHtmlDetailsPage, - ]; - - this.games = discoverGamesFromSteamWeb(this.steamApps, this.htmlDetailsPages); - }); - - it("the length of games is 1", function () { - expect(this.games.length).toBe(2); - }); - - it("the name of the first game array entry is 'Animaddicts'", function () { - expect(this.games[0].name).toBe("Animaddicts"); - }); - - it("the first entry in the games array is an instance of game", function () { - expect(this.games[0]).toBeInstanceOf(Game); - }); - - it("the name of the second game array entry is 'Feartress'", function () { - expect(this.games[1].name).toBe("Feartress"); - }); - - it("the first entry in the games array is an instance of game", function () { - expect(this.games[1]).toBeInstanceOf(Game); - }); - }); - }); - - describe(".getReleaseDate checks for a release date in the provided HTML page.", function () { - describe("if the provided HTML page does not include a release date section,", function () { - beforeAll(function () { - this.result = getReleaseDate(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("the result is an empty string", function () { - expect(this.result).toBe(""); - }); - }); - - describe("if the provided HTML page includes a release date,", function () { - beforeAll(function () { - this.result = getReleaseDate(mortalDarknessGameHtmlDetailsPage); - }); - - it("the result is a date", function () { - expect(this.result).toBeInstanceOf(Date); - }); - it("the result is the correct date'", function () { - expect(this.result.toISOString()).toEqual("2023-08-01T00:00:00.000Z"); - }); - }); - }); - - describe(".getDevelopers checks for developers in the provided HTML page.", function () { - describe("if the provided HTML page does not include any developers,", function () { - beforeAll(function () { - this.result = getDevelopers(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("the result is an empty array", function () { - expect(this.result).toEqual([]); - }); - }); - - describe("if the provided HTML page includes one developer,", function () { - beforeAll(function () { - this.result = getDevelopers(mortalDarknessGameHtmlDetailsPage); - }); - - it("the result is an array with one value", function () { - expect(this.result.length).toBe(1); - }); - - it("the result is 'Dark Faction Games'", function () { - expect(this.result[0]).toBe("Dark Faction Games"); - }); - }); - - describe("if the provided HTML page includes two developers,", function () { - beforeAll(function () { - this.result = getDevelopers(crusaderKingsDetailsPage); - }); - - it("the result is an array with two values", function () { - expect(this.result.length).toBe(2); - }); - - it("the first result is 'Paradox Development Studio'", function () { - expect(this.result[0]).toBe("Paradox Development Studio"); - }); - - it("the second result is 'Paradox Thalassic'", function () { - expect(this.result[1]).toBe("Paradox Thalassic"); - }); - }); - }); - - describe(".getGenres checks for genres in the provided HTML page.", function () { - describe("if the provided HTML page does not include any genres,", function () { - beforeAll(function () { - this.result = getGenres(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("the result is an empty array", function () { - expect(this.result).toEqual([]); - }); - }); - - describe("if the provided HTML page includes genres,", function () { - beforeAll(function () { - this.result = getGenres(mortalDarknessGameHtmlDetailsPage); - }); - - it("the resulting array has a length of 4", function () { - expect(this.result.length).toBe(4); - }); - it("the first result is 'Action'", function () { - expect(this.result[0]).toBe("Action"); - }); - it("the fourth result is 'RPG'", function () { - expect(this.result[3]).toBe("RPG"); - }); - }); - }); - - describe(".getDescription checks for a game's description in the provided HTML page.", function () { - describe("if the provided HTML page does not include a game description,", function () { - beforeAll(function () { - this.result = getGameDescription(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("the result is an empty string", function () { - expect(this.result).toEqual(""); - }); - }); - - describe("if the provided HTML page includes a description,", function () { - beforeAll(function () { - this.result = getGameDescription(mortalDarknessGameHtmlDetailsPage); - }); - - it("the resulting is a specific string", function () { - expect(this.result).toBe( - "“One grim dawn and noble I wake, The darkness is rampant, our oath shall break. A noble warrior soon shall rise, and clear the air of the darkened skies.”", - ); - }); - }); - }); - - describe(".updateTypeSideEffectFree", function () { - describe("discovers one steamApp out of a batch of one, so", function () { - beforeAll(function () { - this.apps = [ - { - appid: 1, - name: "Glitchhikers Soundtrack 2", - }, - ]; - - this.steamApps = SteamApp.manyFromSteamApi(this.apps); - - this.htmlDetailsPages = [glitchhikersSoundtrackHtmlDetailsPage]; - - this.updatedSteamApps = updateTypeSideEffectFree( - this.steamApps, - this.htmlDetailsPages, - ); - }); - - it("the length of updatedSteamApps is 1", function () { - expect(this.updatedSteamApps.length).toBe(1); - }); - - it("the first entry in the updatedSteamApps array is an instance of SteamApp", function () { - expect(this.updatedSteamApps[0]).toBeInstanceOf(SteamApp); - }); - - it("the name of the first updatedSteamApps array entry is 'Glitchhikers Soundtrack 2'", function () { - expect(this.updatedSteamApps[0].name).toBe(this.apps[0].name); - }); - - it("the first array entry in updatedSteamApps has a property 'type', and it's value is 'unknown'", function () { - expect(this.updatedSteamApps[0].type).toBe(SteamApp.validTypes.unknown); - }); - }); - - describe("discovers one game and one downloadable content out of a batch of three steamApps,", function () { - describe("with one html details page being empty", function () { - beforeAll(function () { - this.apps = [ - { - appid: 1, - name: "Glitchhikers Soundtrack 2", - }, - { - appid: 2, - name: "Animaddicts", - }, - { - appid: 3, - name: "The Sims 4 Cats and Dogs DLC", - }, - ]; - - this.steamApps = SteamApp.manyFromSteamApi(this.apps); - - this.htmlDetailsPages = [ - "", - animaddicts2gameHtmlDetailsPage, - theSims4dlcHtmlDetailsPage, - ]; - - this.updatedSteamApps = updateTypeSideEffectFree( - this.steamApps, - this.htmlDetailsPages, - ); - }); - - it("the length of updatedSteamApps is 3", function () { - expect(this.updatedSteamApps.length).toBe(3); - }); - - it("the first entry in the updatedSteamApps array is an instance of SteamApp", function () { - expect(this.updatedSteamApps[0]).toBeInstanceOf(SteamApp); - }); - - it("the second entry in the updatedSteamApps array is an instance of SteamApp", function () { - expect(this.updatedSteamApps[1]).toBeInstanceOf(SteamApp); - }); - - it("the third entry in the updatedSteamApps array is an instance of SteamApp", function () { - expect(this.updatedSteamApps[2]).toBeInstanceOf(SteamApp); - }); - - it("the name of the first updatedSteamApps array entry is 'Glitchhikers Soundtrack 2'", function () { - expect(this.updatedSteamApps[0].name).toBe(this.apps[0].name); - }); - - it("the name of the second updatedSteamApps array entry is 'Animaddicts'", function () { - expect(this.updatedSteamApps[1].name).toBe(this.apps[1].name); - }); - - it("the name of the third updatedSteamApps array entry is 'The Sims 4 Cats and Dogs DLC'", function () { - expect(this.updatedSteamApps[2].name).toBe(this.apps[2].name); - }); - - it("the first array entry in updatedSteamApps has a property 'triedVia', and it's value is 'steamWeb'", function () { - expect(this.updatedSteamApps[0].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - }); - - it("the second array entry in updatedSteamApps has a property 'triedVia', and it's value is 'steamWeb'", function () { - expect(this.updatedSteamApps[1].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - }); - - it("the third array entry in updatedSteamApps has a property 'triedVia', and it's value is 'steamWeb'", function () { - expect(this.updatedSteamApps[2].triedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - }); - - it("the first array entry in updatedSteamApps has a property 'failedVia', and it's value is 'steamWeb'", function () { - expect(this.updatedSteamApps[0].failedVia[0]).toBe( - ValidDataSources.validDataSources.steamWeb, - ); - }); - - it("the first entry in updatedSteamApps has a property 'type', and it's value is 'unknown'", function () { - expect(this.updatedSteamApps[0].type).toBe(SteamApp.validTypes.unknown); - }); - - it("the second entry in updatedSteamApps has a property 'type', and it's value is 'game'", function () { - expect(this.updatedSteamApps[1].type).toBe(SteamApp.validTypes.game); - }); - - it("the third entry in updatedSteamApps has a property 'type', and it's value is 'downloadableContent'", function () { - expect(this.updatedSteamApps[2].type).toBe( - SteamApp.validTypes.downloadableContent, - ); - }); - }); - }); - }); - - describe(".identifyGames", function () { - describe("checks which steam apps are games and instantiates them, so", function () { - describe("when no games out of a batch of one steamApp is passed in,", function () { - beforeAll(function () { - this.updatedSteamApps = [ - { - appid: 1, - name: "Glitchiker Soundtrack", - }, - ]; - - this.instantiatedApps = SteamApp.manyFromSteamApi(this.updatedSteamApps); - this.result = identifyGames(this.instantiatedApps); - }); - - it("the method returns an empty array", function () { - expect(this.result).toEqual([]); - }); - }); - - describe("when one game out of a batch of two steamApps is passed in,", function () { - beforeAll(function () { - this.updatedSteamApps = [ - { - appid: 1, - name: "Glitchiker Soundtrack", - }, - { - appid: 2, - name: "Feartress", - }, - ]; - - this.instantiatedApps = SteamApp.manyFromSteamApi(this.updatedSteamApps); - this.instantiatedApps[1].type = SteamApp.validTypes.game; - - this.result = identifyGames(this.instantiatedApps); - }); - - it("the returned array length is one", function () { - expect(this.result.length).toBe(1); - }); - - it("the name property in the first returned array index is 'Feartress'", function () { - expect(this.result[0].name).toBe(this.updatedSteamApps[1].name); - }); - }); - - describe("when two games out of a batch of three steamApps is passed in,", function () { - beforeAll(function () { - this.updatedSteamApps = [ - { - appid: 1, - name: "Glitchiker Soundtrack", - }, - { - appid: 2, - name: "Feartress", - }, - { - appid: 3, - name: "Elden Ring", - }, - ]; - - this.instantiatedApps = SteamApp.manyFromSteamApi(this.updatedSteamApps); - this.instantiatedApps[1].type = SteamApp.validTypes.game; - this.instantiatedApps[2].type = SteamApp.validTypes.game; - - this.result = identifyGames(this.instantiatedApps); - }); - - it("the returned array length is two", function () { - expect(this.result.length).toBe(2); - }); - - it("the name property in the first returned array index is 'Feartress'", function () { - expect(this.result[0].name).toBe(this.updatedSteamApps[1].name); - }); - - it("the name property in the first returned array index is 'Elden Ring'", function () { - expect(this.result[1].name).toBe(this.updatedSteamApps[2].name); - }); - }); - }); - }); - - describe(".assignType", function () { - describe("checks if result contains a value. If so, sets the type property to 'games'. So", function () { - describe("when result does not contain a value,", function () { - beforeAll(function () { - this.app = { id: 1, name: "Feartress" }; - this.steamApp = SteamApp.oneFromSteamApi(this.app); - - this.result = assignType(undefined, this.steamApp); - }); - - it("the function returns the steamApp. The type property remains 'unknown'", function () { - expect(this.result.type).toBe(SteamApp.validTypes.unknown); - }); - }); - - describe("when the result contains a value,", function () { - beforeAll(function () { - this.app = { id: 1, name: "Feartress" }; - this.steamApp = SteamApp.oneFromSteamApi(this.app); - - this.result = assignType(true, this.steamApp); - }); - - it("the function returns the steamApp. The type property is 'game'", function () { - expect(this.result.type).toBe(SteamApp.validTypes.game); - }); - }); - }); - }); - - describe(".recordAttemptsViaSteamDb", function () { - describe("if one of the pages is empty", function () { - beforeAll(function () { - this.apps = getXSampleSteamApps(2); - - this.pages = createHtmlDetailsPages([counterStrikeHtmlDetailsSteamDb, ""]); - - this.result = recordAttemptsViaSteamDb(this.apps, this.pages); - }); - - it("the first app has the correct values", function () { - expect(this.result[0].triedVia).toEqual([ - ValidDataSources.validDataSources.steamDb, - ]); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second app has the correct values", function () { - expect(this.result[1].triedVia).toEqual([ - ValidDataSources.validDataSources.steamDb, - ]); - expect(this.result[1].failedVia).toEqual([ - ValidDataSources.validDataSources.steamDb, - ]); - }); - }); - - describe("if none of the pages are empty", function () { - beforeAll(function () { - this.apps = getXSampleSteamApps(2); - - this.pages = createHtmlDetailsPages([ - counterStrikeHtmlDetailsSteamDb, - karmazooHtmlDetailsPageSteamDb, - ]); - - this.result = recordAttemptsViaSteamDb(this.apps, this.pages); - }); - - it("the first app has the correct values", function () { - expect(this.result[0].triedVia).toEqual([ - ValidDataSources.validDataSources.steamDb, - ]); - expect(this.result[0].failedVia).toEqual([]); - }); - - it("the second app has the correct values", function () { - expect(this.result[1].triedVia).toEqual([ - ValidDataSources.validDataSources.steamDb, - ]); - expect(this.result[1].failedVia).toEqual([]); - }); - }); - - describe("when the result contains a value,", function () { - beforeAll(function () { - this.app = getXSampleSteamApps(1); - this.steamApp = SteamApp.oneFromSteamApi(this.app); - - this.result = assignType(true, this.steamApp); - }); - - it("the function returns the steamApp. The type property is 'game'", function () { - expect(this.result.type).toBe(SteamApp.validTypes.game); - }); - }); - }); - - describe(".updateMissingDetails.", function () { - describe("When we try to update two games with missing details,", function () { - beforeAll(function () { - this.games = getXsteamchartsInstantiatedGames(2); - - const htmlDetailsPages = createHtmlDetailsPages([ - counterStrikeHtmlDetailsSteamDb, - riskOfRainHtmlDetailsSteamDb, - ]); - - updateMissingDetails(this.games, htmlDetailsPages); - }); - - it("two games are returned", function () { - expect(this.games.length).toBe(2); - }); - - it("the first game's details are updated", function () { - expect(this.games[0].developers).toEqual(["Valve", "Hidden Path Entertainment"]); - expect(this.games[0].genres).toEqual(["Action", "Free to Play"]); - expect(this.games[0].description).toBe( - "Counter-Strike: Global Offensive (CS: GO) expands upon the team-based action gameplay that it pioneered when it was launched 19 years ago. CS: GO features new maps, characters, weapons, and game modes, and delivers updated versions of the classic CS content (de_dust2, etc.).", - ); - }); - - it("the second game's details are updated", function () { - expect(this.games[1].developers).toEqual(["Hopoo Games"]); - expect(this.games[1].genres).toEqual(["Action", "Indie"]); - expect(this.games[1].description).toBe( - "Escape a chaotic alien planet by fighting through hordes of frenzied monsters – with your friends, or on your own. Combine loot in surprising ways and master each character until you become the havoc you feared upon your first crash landing.", - ); - }); - }); - }); - - describe(".getSteamDbReleaseDate.", function () { - describe("When we provide a html page that doesn't contain a valid date,", function () { - beforeAll(function () { - this.result = getSteamDbReleaseDate(karmazooHtmlDetailsPageSteamDb); - }); - - it("an empty string is returned", function () { - expect(this.result).toBe(""); - }); - }); - - describe("When we provide a html page that contains a valid date,", function () { - beforeAll(function () { - this.date = new Date("11 August 2020 UTC"); - - this.result = getSteamDbReleaseDate(riskOfRainHtmlDetailsSteamDb); - }); - - it("a date is returned'", function () { - expect(this.result).toBeInstanceOf(Date); - }); - - it("the result is the correct date", function () { - expect(this.result.toISOString()).toEqual("2020-08-11T00:00:00.000Z"); - }); - }); - - describe("When we provide a html page that doesn't contain a date section", function () { - beforeAll(function () { - this.result = getSteamDbReleaseDate(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("an empty string is returned", function () { - expect(this.result).toBe(""); - }); - }); - }); - - describe(".getSteamDbDevelopers.", function () { - describe("When we provide a html page that contains two developers,", function () { - beforeAll(function () { - this.result = getSteamDbDevelopers(counterStrikeHtmlDetailsSteamDb); - }); - - it("two developers are returned", function () { - expect(this.result.length).toBe(2); - }); - - it("the developer is 'Valve'", function () { - expect(this.result[0]).toBe("Valve"); - }); - - it("the developer is 'Hidden Path Entertainment'", function () { - expect(this.result[1]).toBe("Hidden Path Entertainment"); - }); - }); - - describe("When we provide a html page that doesn't contain a developer section", function () { - beforeAll(function () { - this.result = getSteamDbDevelopers(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("an empty array is returned", function () { - expect(this.result).toEqual([]); - }); - }); - }); - - describe(".getSteamDbGenres.", function () { - describe("When we provide a html page that contains the genres,", function () { - beforeAll(function () { - this.result = getSteamDbGenres(riskOfRainHtmlDetailsSteamDb); - }); - - it("two genres are returned", function () { - expect(this.result.length).toBe(2); - }); - - it("the first genre is 'Action'", function () { - expect(this.result[0]).toBe("Action"); - }); - - it("the second genre is 'Indie'", function () { - expect(this.result[1]).toBe("Indie"); - }); - }); - - describe("When we provide a html page that doesn't contain a genres section,", function () { - beforeAll(function () { - this.result = getSteamDbGenres(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("an empty array is returned", function () { - expect(this.result).toEqual([]); - }); - }); - }); - - describe(".getSteamDbDescription.", function () { - describe("When we provide a html page that contains the description,", function () { - beforeAll(function () { - this.result = getSteamDbDescription(riskOfRainHtmlDetailsSteamDb); - }); - - it("the returned value is the game's description'", function () { - expect(this.result).toEqual( - "Escape a chaotic alien planet by fighting through hordes of frenzied monsters – with your friends, or on your own. Combine loot in surprising ways and master each character until you become the havoc you feared upon your first crash landing.", - ); - }); - }); - - describe("When we provide a html page that doesn't contain a description section,", function () { - beforeAll(function () { - this.result = getSteamDbDescription(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("an empty string is returned", function () { - expect(this.result).toEqual(""); - }); - }); - }); - - describe(".updateMissingReleaseDates.", function () { - describe("When we try to update two games with missing release dates,", function () { - beforeAll(function () { - this.games = getXsteamchartsInstantiatedGames(2); - - const htmlDetailsPages = createHtmlDetailsPages([ - counterStrikeHtmlDetailsSteamDb, - riskOfRainHtmlDetailsSteamDb, - ]); - - updateMissingReleaseDates(this.games, htmlDetailsPages); - }); - - it("two games are returned", function () { - expect(this.games.length).toBe(2); - }); - - it("the first game's release date is updated", function () { - expect(this.games[0].releaseDate).toEqual(new Date("21 August 2012 UTC")); - }); - - it("the second game's release date is updated", function () { - expect(this.games[1].releaseDate).toEqual(new Date("11 August 2020 UTC")); - }); - }); - }); - - describe(".getSteamDbReleaseDate.", function () { - describe("When we provide a html page that doesn't contain a valid release date,", function () { - beforeAll(function () { - this.result = getSteamDbReleaseDate(karmazooHtmlDetailsPageSteamDb); - }); - - it("an empty string is returned", function () { - expect(this.result).toBe(""); - }); - }); - - describe("When we provide a html page that contains a valid release date,", function () { - beforeAll(function () { - this.date = new Date("11 August 2020 UTC"); - - this.result = getSteamDbReleaseDate(riskOfRainHtmlDetailsSteamDb); - }); - - it("a date is returned'", function () { - expect(this.result).toBeInstanceOf(Date); - }); - - it("the correct date is returned", function () { - expect(this.result).toEqual(this.date); - }); - }); - - describe("When we provide a html page that doesn't contain a date section", function () { - beforeAll(function () { - this.result = getSteamDbReleaseDate(riskOfRainHtmlDetailsPageMissingInfo); - }); - - it("an empty string is returned", function () { - expect(this.result).toBe(""); - }); - }); - }); -}); diff --git a/backend/src/main.js b/backend/src/main.js index 879d6075e..2ac048cac 100644 --- a/backend/src/main.js +++ b/backend/src/main.js @@ -1,12 +1,13 @@ import httpClient from "axios"; import { DatabaseClient } from "./adapters/driven/db/database.client.js"; import { SteamClient } from "./adapters/driven/http/steam.client.js"; -import { SteamAppsAggregator } from "./core/features/steam-apps-aggregator/steam.apps.aggregator.js"; -import { GameIdentifier } from "./core/features/game-identifier/game.identifier.js"; -import { PlayerHistoryAggregator } from "./core/features/player-history-aggregator/player.history.aggregator.js"; +import { Runner } from "./adapters/driving/runner/runner.js"; import { WebServer } from "./adapters/driving/rest/web.server.js"; import { GameQueriesController } from "./adapters/driving/rest/game-queries/game.queries.controller.js"; import { GameQueriesRouter } from "./adapters/driving/rest/game-queries/game.queries.router.js"; +import { SteamAppsAggregator } from "./core/features/steam-apps-aggregator/steam.apps.aggregator.js"; +import { GameIdentifier } from "./core/features/game-identifier/game.identifier.js"; +import { PlayerHistoryAggregator } from "./core/features/player-history-aggregator/player.history.aggregator.js"; import { GamesRepository } from "./core/repositories/games.repository.js"; import { SteamAppsRepository } from "./core/repositories/steam.apps.repository.js"; import { SteamAppsUpdateTimestampsRepository } from "./core/repositories/steam.apps.update.timestamps.repository.js"; @@ -14,7 +15,8 @@ import { PlayerHistoryRepository } from "./core/repositories/player.history.repo import { HistoryChecksRepository } from "./core/repositories/history.checks.repository.js"; import { config } from "./common/config.loader.js"; import { Logger } from "./common/logger.js"; -import { Runner } from "./adapters/driving/runner/runner.js"; +import { ValidDataSources } from "./core/models/valid.data.sources.js"; +import { parseHTML } from "linkedom"; // our entry point = main async function main() { @@ -48,6 +50,7 @@ async function main() { historyChecksRepository, logger, config.features, + parseHTML, ); const playerHistoryAggregator = new PlayerHistoryAggregator( steamClient, @@ -73,11 +76,15 @@ async function main() { ? { func: steamAppsAggregator.collectSteamAppsDiffOnDbLayer } : { func: steamAppsAggregator.collectSteamApps }; + const steamWeb = ValidDataSources.validDataSources.steamWeb; + const steamcharts = ValidDataSources.validDataSources.steamcharts; + const runnables = [ steamAppsAggregatorRunnable, - { func: gameIdentifier.tryViaSteamWeb }, - { func: gameIdentifier.tryViaSteamchartsWeb }, + { func: () => gameIdentifier.checkIfGameViaSource(steamWeb) }, + { func: () => gameIdentifier.checkIfGameViaSource(steamcharts) }, { func: gameIdentifier.updateGamesWithoutDetails }, + { func: gameIdentifier.updateGamesWithoutReleaseDates }, { func: playerHistoryAggregator.addPlayerHistoryFromSteamcharts }, { func: playerHistoryAggregator.addCurrentPlayers }, ];