From f4880d05198609f866b977e8574678267527b7d4 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Tue, 14 Nov 2023 10:01:31 -0800 Subject: [PATCH 1/5] more games information, games route (#756) * more games information, games route * adds banner url --- ...ad9944f35b4554d340bddd0ad32782f193b3.json} | 4 +- ...9a6c6fbaa555e112db3adf7accace9b55df5.json} | 4 +- ...3f6817ce05fd3212b900f0d437ca42decd47.json} | 4 +- ...1174fbeb9e8399bdcc78ffe8f80181b217a4.json} | 4 +- ...fa8726ea6f4862febc4b650f643393a45cb8.json} | 4 +- ...9635bddfe43342ee549fad2433a271f8feeee.json | 44 +++++++++++ migrations/20231113104902_games_metadata.sql | 9 +++ src/database/models/ids.rs | 3 + src/database/models/loader_fields.rs | 77 +++++++++++++------ src/database/models/project_item.rs | 2 +- src/database/models/version_item.rs | 2 +- src/routes/v3/project_creation.rs | 2 +- src/routes/v3/tags.rs | 35 +++++++-- src/routes/v3/version_creation.rs | 3 +- src/search/indexing/local_import.rs | 2 +- src/util/webhook.rs | 2 +- tests/common/api_v3/mod.rs | 1 + tests/common/api_v3/tags.rs | 26 +++++++ tests/games.rs | 23 ++++++ 19 files changed, 206 insertions(+), 45 deletions(-) rename .sqlx/{query-3afbc93a8945e7ae07e39a88752f400c06f9c8a8132fd7a05dcc55c6eab5d2e7.json => query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json} (97%) rename .sqlx/{query-cab90ea34929643f9e9814150c4dbd027fc0bd427bfba5e6eb99c989af53b680.json => query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json} (97%) rename .sqlx/{query-f73ffab12a96eb9480615e333d40cde031df280039cd8e435cfca5e15ed3d1c4.json => query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json} (97%) rename .sqlx/{query-923d1d1e5e9b879479a244479952df15841d35b96fbdcadc7d5af8d6b4671f9e.json => query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json} (90%) rename .sqlx/{query-f7aee6fbd3415c7819d9ae1a75a0ae5753aaa3373c3ac9bc04adb3087781b49f.json => query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json} (97%) create mode 100644 .sqlx/query-ee2924461357098fd535608f5219635bddfe43342ee549fad2433a271f8feeee.json create mode 100644 migrations/20231113104902_games_metadata.sql create mode 100644 tests/common/api_v3/tags.rs create mode 100644 tests/games.rs diff --git a/.sqlx/query-3afbc93a8945e7ae07e39a88752f400c06f9c8a8132fd7a05dcc55c6eab5d2e7.json b/.sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json similarity index 97% rename from .sqlx/query-3afbc93a8945e7ae07e39a88752f400c06f9c8a8132fd7a05dcc55c6eab5d2e7.json rename to .sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json index 0ecfb803..57fc1874 100644 --- a/.sqlx/query-3afbc93a8945e7ae07e39a88752f400c06f9c8a8132fd7a05dcc55c6eab5d2e7.json +++ b/.sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, pt.id, u.id;\n ", + "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, pt.id, u.id;\n ", "describe": { "columns": [ { @@ -122,5 +122,5 @@ null ] }, - "hash": "3afbc93a8945e7ae07e39a88752f400c06f9c8a8132fd7a05dcc55c6eab5d2e7" + "hash": "2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3" } diff --git a/.sqlx/query-cab90ea34929643f9e9814150c4dbd027fc0bd427bfba5e6eb99c989af53b680.json b/.sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json similarity index 97% rename from .sqlx/query-cab90ea34929643f9e9814150c4dbd027fc0bd427bfba5e6eb99c989af53b680.json rename to .sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json index 0951bcc2..cbfea166 100644 --- a/.sqlx/query-cab90ea34929643f9e9814150c4dbd027fc0bd427bfba5e6eb99c989af53b680.json +++ b/.sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, pt.id, u.id;\n ", + "query": "\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, pt.id, u.id;\n ", "describe": { "columns": [ { @@ -176,5 +176,5 @@ null ] }, - "hash": "cab90ea34929643f9e9814150c4dbd027fc0bd427bfba5e6eb99c989af53b680" + "hash": "2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5" } diff --git a/.sqlx/query-f73ffab12a96eb9480615e333d40cde031df280039cd8e435cfca5e15ed3d1c4.json b/.sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json similarity index 97% rename from .sqlx/query-f73ffab12a96eb9480615e333d40cde031df280039cd8e435cfca5e15ed3d1c4.json rename to .sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json index 91e7b818..2307d9fd 100644 --- a/.sqlx/query-f73ffab12a96eb9480615e333d40cde031df280039cd8e435cfca5e15ed3d1c4.json +++ b/.sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m \n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT JOIN loaders l on lv.loader_id = l.id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", + "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m \n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT JOIN loaders l on lv.loader_id = l.id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", "describe": { "columns": [ { @@ -230,5 +230,5 @@ null ] }, - "hash": "f73ffab12a96eb9480615e333d40cde031df280039cd8e435cfca5e15ed3d1c4" + "hash": "8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47" } diff --git a/.sqlx/query-923d1d1e5e9b879479a244479952df15841d35b96fbdcadc7d5af8d6b4671f9e.json b/.sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json similarity index 90% rename from .sqlx/query-923d1d1e5e9b879479a244479952df15841d35b96fbdcadc7d5af8d6b4671f9e.json rename to .sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json index 65c31f42..14c648d9 100644 --- a/.sqlx/query-923d1d1e5e9b879479a244479952df15841d35b96fbdcadc7d5af8d6b4671f9e.json +++ b/.sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT l.id id, l.loader loader, l.icon icon,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games\n FROM loaders l \n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg ON lptg.loader_id = lpt.joining_loader_id AND lptg.project_type_id = lpt.joining_project_type_id\n LEFT OUTER JOIN games g ON lptg.game_id = g.id\n GROUP BY l.id;\n ", + "query": "\n SELECT l.id id, l.loader loader, l.icon icon,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games\n FROM loaders l \n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg ON lptg.loader_id = lpt.joining_loader_id AND lptg.project_type_id = lpt.joining_project_type_id\n LEFT OUTER JOIN games g ON lptg.game_id = g.id\n GROUP BY l.id;\n ", "describe": { "columns": [ { @@ -40,5 +40,5 @@ null ] }, - "hash": "923d1d1e5e9b879479a244479952df15841d35b96fbdcadc7d5af8d6b4671f9e" + "hash": "a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4" } diff --git a/.sqlx/query-f7aee6fbd3415c7819d9ae1a75a0ae5753aaa3373c3ac9bc04adb3087781b49f.json b/.sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json similarity index 97% rename from .sqlx/query-f7aee6fbd3415c7819d9ae1a75a0ae5753aaa3373c3ac9bc04adb3087781b49f.json rename to .sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json index d67b3518..ae83be3b 100644 --- a/.sqlx/query-f7aee6fbd3415c7819d9ae1a75a0ae5753aaa3373c3ac9bc04adb3087781b49f.json +++ b/.sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies,\n \n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', l.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n \n FROM versions v\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN loaders_project_types lpt on l.id = lpt.joining_loader_id\n LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id\n LEFT JOIN games g on lptg.game_id = g.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfe.id = lfev.enum_id\n\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies,\n \n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', l.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n \n FROM versions v\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN loaders_project_types lpt on l.id = lpt.joining_loader_id\n LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id\n LEFT JOIN games g on lptg.game_id = g.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfe.id = lfev.enum_id\n\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", "describe": { "columns": [ { @@ -144,5 +144,5 @@ null ] }, - "hash": "f7aee6fbd3415c7819d9ae1a75a0ae5753aaa3373c3ac9bc04adb3087781b49f" + "hash": "ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8" } diff --git a/.sqlx/query-ee2924461357098fd535608f5219635bddfe43342ee549fad2433a271f8feeee.json b/.sqlx/query-ee2924461357098fd535608f5219635bddfe43342ee549fad2433a271f8feeee.json new file mode 100644 index 00000000..90f78643 --- /dev/null +++ b/.sqlx/query-ee2924461357098fd535608f5219635bddfe43342ee549fad2433a271f8feeee.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, slug, name, icon_url, banner_url FROM games\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "banner_url", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + true, + true + ] + }, + "hash": "ee2924461357098fd535608f5219635bddfe43342ee549fad2433a271f8feeee" +} diff --git a/migrations/20231113104902_games_metadata.sql b/migrations/20231113104902_games_metadata.sql new file mode 100644 index 00000000..0dfed068 --- /dev/null +++ b/migrations/20231113104902_games_metadata.sql @@ -0,0 +1,9 @@ +ALTER TABLE games ADD COLUMN slug varchar(64); +ALTER TABLE games ADD COLUMN icon_url varchar(2048) NULL; +ALTER TABLE games ADD COLUMN banner_url varchar(2048) NULL; + +-- 'minecraft-java' and 'minecraft-bedrock' are the only games- both slug and names (names are for translations) +UPDATE games SET slug = name; +ALTER TABLE games ALTER COLUMN slug SET NOT NULL; +ALTER TABLE games ALTER COLUMN name SET NOT NULL; +ALTER TABLE games ADD CONSTRAINT unique_game_slug UNIQUE (slug); \ No newline at end of file diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 03463976..bd274fb5 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -214,6 +214,9 @@ pub struct StatusId(pub i32); pub struct SideTypeId(pub i32); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] #[sqlx(transparent)] +pub struct GameId(pub i32); +#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)] +#[sqlx(transparent)] pub struct DonationPlatformId(pub i32); #[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index 55d70c07..ca890b26 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -9,35 +9,71 @@ use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; +const GAMES_LIST_NAMESPACE: &str = "games"; const LOADER_ID: &str = "loader_id"; const LOADERS_LIST_NAMESPACE: &str = "loaders"; const LOADER_FIELDS_NAMESPACE: &str = "loader_fields"; const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums"; const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str = "loader_field_enum_values"; -#[derive(Clone, Serialize, Deserialize, Debug, Copy)] -pub enum Game { - MinecraftJava, - // MinecraftBedrock - // Future games +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Game { + pub id: GameId, + pub slug: String, + pub name: String, + pub icon_url: Option, + pub banner_url: Option, } impl Game { - pub fn name(&self) -> &'static str { - match self { - Game::MinecraftJava => "minecraft-java", - // Game::MinecraftBedrock => "minecraft-bedrock" - // Future games - } + pub async fn get_slug<'a, E>( + slug: &str, + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Ok(Self::list(exec, redis) + .await? + .into_iter() + .find(|x| x.slug == slug)) } - pub fn from_name(name: &str) -> Option { - match name { - "minecraft-java" => Some(Game::MinecraftJava), - // "minecraft-bedrock" => Some(Game::MinecraftBedrock) - // Future games - _ => None, + pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let cached_games: Option> = redis + .get_deserialized_from_json(GAMES_LIST_NAMESPACE, "games") + .await?; + if let Some(cached_games) = cached_games { + return Ok(cached_games); } + + let result = sqlx::query!( + " + SELECT id, slug, name, icon_url, banner_url FROM games + ", + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| Game { + id: GameId(x.id), + slug: x.slug, + name: x.name, + icon_url: x.icon_url, + banner_url: x.banner_url, + })) + }) + .try_collect::>() + .await?; + + redis + .set_serialized_to_json(GAMES_LIST_NAMESPACE, "games", &result, None) + .await?; + + Ok(result) } } @@ -47,7 +83,7 @@ pub struct Loader { pub loader: String, pub icon: String, pub supported_project_types: Vec, - pub supported_games: Vec, + pub supported_games: Vec, // slugs } impl Loader { @@ -99,7 +135,7 @@ impl Loader { " SELECT l.id id, l.loader loader, l.icon icon, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, - ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games + ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games FROM loaders l LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id @@ -123,9 +159,6 @@ impl Loader { supported_games: x .games .unwrap_or_default() - .iter() - .filter_map(|x| Game::from_name(x)) - .collect(), })) }) .try_collect::>() diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index a7589a3d..61dd2464 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -571,7 +571,7 @@ impl Project { t.id thread_id, m.monetization_status monetization_status, ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, - ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games, + ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions, diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 9cff920b..b01106e0 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -524,7 +524,7 @@ impl Version { v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering, ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, - ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games, + ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files, JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes, JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies, diff --git a/src/routes/v3/project_creation.rs b/src/routes/v3/project_creation.rs index 637aa46a..a762b168 100644 --- a/src/routes/v3/project_creation.rs +++ b/src/routes/v3/project_creation.rs @@ -796,7 +796,7 @@ async fn project_create_inner( |(mut project_types, mut games), loader| { if loaders.contains(&loader.id) { project_types.extend(loader.supported_project_types); - games.extend(loader.supported_games.iter().map(|x| x.name().to_string())); + games.extend(loader.supported_games); } (project_types, games) }, diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index a82beaf1..4308c7dd 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use super::ApiError; use crate::database::models::categories::{Category, DonationPlatform, ProjectType, ReportType}; use crate::database::models::loader_fields::{ - Loader, LoaderField, LoaderFieldEnumValue, LoaderFieldType, + Game, Loader, LoaderField, LoaderFieldEnumValue, LoaderFieldType, }; use crate::database::redis::RedisPool; use actix_web::{web, HttpResponse}; @@ -16,6 +16,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("category", web::get().to(category_list)) .route("loader", web::get().to(loader_list)), ) + .route("games", web::get().to(games_list)) .route("loader_fields", web::get().to(loader_fields_list)) .route("license", web::get().to(license_list)) .route("license/{id}", web::get().to(license_text)) @@ -24,6 +25,32 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("project_type", web::get().to(project_type_list)); } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct GameData { + pub slug: String, + pub name: String, + pub icon: Option, + pub banner: Option, +} + +pub async fn games_list( + pool: web::Data, + redis: web::Data, +) -> Result { + let results = Game::list(&**pool, &redis) + .await? + .into_iter() + .map(|x| GameData { + slug: x.slug, + name: x.name, + icon: x.icon_url, + banner: x.banner_url, + }) + .collect::>(); + + Ok(HttpResponse::Ok().json(results)) +} + #[derive(serde::Serialize, serde::Deserialize)] pub struct CategoryData { pub icon: String, @@ -69,11 +96,7 @@ pub async fn loader_list( icon: x.icon, name: x.loader, supported_project_types: x.supported_project_types, - supported_games: x - .supported_games - .iter() - .map(|x| x.name().to_string()) - .collect(), + supported_games: x.supported_games, }) .collect::>(); diff --git a/src/routes/v3/version_creation.rs b/src/routes/v3/version_creation.rs index 524e0c45..106b956d 100644 --- a/src/routes/v3/version_creation.rs +++ b/src/routes/v3/version_creation.rs @@ -423,8 +423,7 @@ async fn version_create_inner( let (all_project_types, all_games): (Vec, Vec) = loader_structs.iter().fold((vec![], vec![]), |mut acc, x| { acc.0.extend_from_slice(&x.supported_project_types); - acc.1 - .extend(x.supported_games.iter().map(|x| x.name().to_string())); + acc.1.extend(x.supported_games.clone()); acc }); diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 3b3c80f8..b2bf89af 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -27,7 +27,7 @@ pub async fn index_local( ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, - ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games, + ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery, JSONB_AGG( diff --git a/src/util/webhook.rs b/src/util/webhook.rs index aa8c7480..0e5f6ad2 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -91,7 +91,7 @@ pub async fn send_discord_webhook( ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories, ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, - ARRAY_AGG(DISTINCT g.name) filter (where g.name is not null) games, + ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery, JSONB_AGG( diff --git a/tests/common/api_v3/mod.rs b/tests/common/api_v3/mod.rs index 2155aa3c..e0c36798 100644 --- a/tests/common/api_v3/mod.rs +++ b/tests/common/api_v3/mod.rs @@ -6,6 +6,7 @@ use std::rc::Rc; pub mod oauth; pub mod oauth_clients; +pub mod tags; #[derive(Clone)] pub struct ApiV3 { diff --git a/tests/common/api_v3/tags.rs b/tests/common/api_v3/tags.rs new file mode 100644 index 00000000..dd36ad74 --- /dev/null +++ b/tests/common/api_v3/tags.rs @@ -0,0 +1,26 @@ +use actix_web::{ + dev::ServiceResponse, + test::{self, TestRequest}, +}; +use labrinth::routes::v3::tags::GameData; + +use crate::common::database::ADMIN_USER_PAT; + +use super::ApiV3; + +impl ApiV3 { + // TODO: fold this into v3 API of other v3 testing PR + pub async fn get_games(&self) -> ServiceResponse { + let req = TestRequest::get() + .uri("/v3/games") + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + self.call(req).await + } + + pub async fn get_games_deserialized(&self) -> Vec { + let resp = self.get_games().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} diff --git a/tests/games.rs b/tests/games.rs new file mode 100644 index 00000000..14ddb378 --- /dev/null +++ b/tests/games.rs @@ -0,0 +1,23 @@ +// TODO: fold this into loader_fields.rs or tags.rs of other v3 testing PR + +use crate::common::environment::TestEnvironment; + +mod common; + +#[actix_rt::test] +async fn get_games() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v3; + + let games = api.get_games_deserialized().await; + + // There should be 2 games in the dummy data + assert_eq!(games.len(), 2); + assert_eq!(games[0].name, "minecraft-java"); + assert_eq!(games[1].name, "minecraft-bedrock"); + + assert_eq!(games[0].slug, "minecraft-java"); + assert_eq!(games[1].slug, "minecraft-bedrock"); + + test_env.cleanup().await; +} From 74973e73e6589d77fcf48c26d9d79073d5a08f23 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Thu, 16 Nov 2023 10:36:03 -0800 Subject: [PATCH 2/5] Tests 3 restructure (#754) * moved files * moved files * initial v3 additions * moves req data * tests passing, restructuring, remove v2 * fmt; clippy; prepare * merge conflicts + issues * merge conflict, fmt, clippy, prepare * revs * fixed failing test * fixed tests --- ...2ad9944f35b4554d340bddd0ad32782f193b3.json | 126 ---- ...89a6c6fbaa555e112db3adf7accace9b55df5.json | 180 ------ ...24306e9fcae87e6eac458e7f813ebb57c78c4.json | 120 ++++ ...15fa69bd328f44b4bf991e80f6b91fcd3a50.json} | 16 +- ...7760e1acfa9c41e157f6d75d2538abdba5e4e.json | 174 ++++++ ...0241110010322_adds_game_version_minmax.sql | 1 + src/auth/oauth/mod.rs | 13 +- src/database/models/loader_fields.rs | 138 +++-- src/models/v3/teams.rs | 2 +- src/routes/v2/mod.rs | 1 + src/routes/v2/projects.rs | 8 +- src/routes/v2/version_creation.rs | 5 + src/routes/v3/admin.rs | 322 +++++++++++ src/routes/v3/mod.rs | 17 +- src/routes/v3/oauth_clients.rs | 8 +- src/routes/v3/organizations.rs | 4 +- src/routes/v3/project_creation.rs | 38 +- src/routes/v3/projects.rs | 4 +- src/routes/v3/tags.rs | 23 +- src/routes/v3/threads.rs | 3 +- src/routes/v3/users.rs | 5 +- src/routes/v3/version_creation.rs | 97 ++-- src/routes/v3/version_file.rs | 8 +- src/routes/v3/versions.rs | 16 +- src/search/indexing/local_import.rs | 6 +- src/search/indexing/mod.rs | 4 +- src/search/mod.rs | 4 +- src/util/webhook.rs | 7 +- tests/analytics.rs | 6 +- tests/common/api_v2/mod.rs | 2 +- tests/common/api_v2/project.rs | 12 +- tests/common/{ => api_v2}/request_data.rs | 2 +- tests/common/api_v2/version.rs | 7 +- tests/common/api_v3/mod.rs | 16 + tests/common/api_v3/oauth.rs | 8 +- .../common/{api_v2 => api_v3}/organization.rs | 28 +- tests/common/api_v3/project.rs | 256 +++++++++ tests/common/api_v3/request_data.rs | 146 +++++ tests/common/api_v3/tags.rs | 49 ++ tests/common/api_v3/team.rs | 176 ++++++ tests/common/api_v3/version.rs | 422 ++++++++++++++ tests/common/asserts.rs | 4 +- tests/common/dummy_data.rs | 51 +- tests/common/environment.rs | 9 +- tests/common/mod.rs | 1 - tests/common/permissions.rs | 159 ++++-- tests/files/dummy_data.sql | 13 + tests/loader_fields.rs | 306 ++++++++++ tests/notifications.rs | 6 +- tests/oauth.rs | 9 +- tests/oauth_clients.rs | 2 +- tests/organizations.rs | 52 +- tests/pats.rs | 38 +- tests/project.rs | 185 ++---- tests/scopes.rs | 196 +++---- tests/search.rs | 57 +- tests/tags.rs | 44 +- tests/teams.rs | 91 ++- tests/user.rs | 11 +- tests/v2/project.rs | 538 ++++++++++++++++++ tests/v2/scopes.rs | 109 ++++ tests/v2/search.rs | 299 ++++++++++ tests/v2/tags.rs | 74 +++ tests/v2/version.rs | 409 +++++++++++++ tests/v2_tests.rs | 16 + tests/version.rs | 58 +- 66 files changed, 4233 insertions(+), 984 deletions(-) delete mode 100644 .sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json delete mode 100644 .sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json create mode 100644 .sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json rename .sqlx/{query-622496d06b9d1e5019d7dcb45ac768558305f1270c1c43ef767f54b9baf5b5af.json => query-bb6afad07ebfa3b92399bb07aa9e15fa69bd328f44b4bf991e80f6b91fcd3a50.json} (60%) create mode 100644 .sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json create mode 100644 migrations/20241110010322_adds_game_version_minmax.sql create mode 100644 src/routes/v3/admin.rs rename tests/common/{ => api_v2}/request_data.rs (98%) rename tests/common/{api_v2 => api_v3}/organization.rs (84%) create mode 100644 tests/common/api_v3/project.rs create mode 100644 tests/common/api_v3/request_data.rs create mode 100644 tests/common/api_v3/team.rs create mode 100644 tests/common/api_v3/version.rs create mode 100644 tests/loader_fields.rs create mode 100644 tests/v2/project.rs create mode 100644 tests/v2/scopes.rs create mode 100644 tests/v2/search.rs create mode 100644 tests/v2/tags.rs create mode 100644 tests/v2/version.rs create mode 100644 tests/v2_tests.rs diff --git a/.sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json b/.sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json deleted file mode 100644 index 57fc1874..00000000 --- a/.sqlx/query-2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n pt.name project_type, u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, pt.id, u.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "color", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "project_type", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "username", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "avatar_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 10, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 11, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 12, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 13, - "name": "gallery", - "type_info": "VarcharArray" - }, - { - "ordinal": 14, - "name": "featured_gallery", - "type_info": "VarcharArray" - }, - { - "ordinal": 15, - "name": "version_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 16, - "name": "loader_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "loader_field_enum_values", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8", - "TextArray", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - false, - true, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "2253e9a36185947199cb5b3909a2ad9944f35b4554d340bddd0ad32782f193b3" -} diff --git a/.sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json b/.sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json deleted file mode 100644 index cbfea166..00000000 --- a/.sqlx/query-2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, pt.id, u.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "version_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "approved", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 11, - "name": "license", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "color", - "type_info": "Int4" - }, - { - "ordinal": 15, - "name": "project_type_name", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "username", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 18, - "name": "additional_categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 19, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 20, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 21, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 22, - "name": "gallery", - "type_info": "VarcharArray" - }, - { - "ordinal": 23, - "name": "featured_gallery", - "type_info": "VarcharArray" - }, - { - "ordinal": 24, - "name": "version_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 25, - "name": "loader_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 26, - "name": "loader_field_enum_values", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "TextArray", - "TextArray", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - false, - true, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "2ac81625d4facd7bbe14a682f0c89a6c6fbaa555e112db3adf7accace9b55df5" -} diff --git a/.sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json b/.sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json new file mode 100644 index 00000000..ffbe1f07 --- /dev/null +++ b/.sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json @@ -0,0 +1,120 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, u.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "color", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "avatar_url", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 9, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 10, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 11, + "name": "games", + "type_info": "VarcharArray" + }, + { + "ordinal": 12, + "name": "gallery", + "type_info": "VarcharArray" + }, + { + "ordinal": 13, + "name": "featured_gallery", + "type_info": "VarcharArray" + }, + { + "ordinal": 14, + "name": "version_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 15, + "name": "loader_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 16, + "name": "loader_field_enum_values", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8", + "TextArray", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + false, + true, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4" +} diff --git a/.sqlx/query-622496d06b9d1e5019d7dcb45ac768558305f1270c1c43ef767f54b9baf5b5af.json b/.sqlx/query-bb6afad07ebfa3b92399bb07aa9e15fa69bd328f44b4bf991e80f6b91fcd3a50.json similarity index 60% rename from .sqlx/query-622496d06b9d1e5019d7dcb45ac768558305f1270c1c43ef767f54b9baf5b5af.json rename to .sqlx/query-bb6afad07ebfa3b92399bb07aa9e15fa69bd328f44b4bf991e80f6b91fcd3a50.json index b92f9f5a..01b0c698 100644 --- a/.sqlx/query-622496d06b9d1e5019d7dcb45ac768558305f1270c1c43ef767f54b9baf5b5af.json +++ b/.sqlx/query-bb6afad07ebfa3b92399bb07aa9e15fa69bd328f44b4bf991e80f6b91fcd3a50.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type\n FROM loader_fields lf\n ", + "query": "\n SELECT DISTINCT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type, lfl.loader_id\n FROM loader_fields lf\n LEFT JOIN loader_fields_loaders lfl ON lfl.loader_field_id = lf.id\n WHERE lfl.loader_id = ANY($1)\n ", "describe": { "columns": [ { @@ -37,10 +37,17 @@ "ordinal": 6, "name": "enum_type", "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "loader_id", + "type_info": "Int4" } ], "parameters": { - "Left": [] + "Left": [ + "Int4Array" + ] }, "nullable": [ false, @@ -49,8 +56,9 @@ false, true, true, - true + true, + false ] }, - "hash": "622496d06b9d1e5019d7dcb45ac768558305f1270c1c43ef767f54b9baf5b5af" + "hash": "bb6afad07ebfa3b92399bb07aa9e15fa69bd328f44b4bf991e80f6b91fcd3a50" } diff --git a/.sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json b/.sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json new file mode 100644 index 00000000..37df9b4a --- /dev/null +++ b/.sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json @@ -0,0 +1,174 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, u.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "approved", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "license", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "color", + "type_info": "Int4" + }, + { + "ordinal": 15, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 17, + "name": "additional_categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 18, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 19, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 20, + "name": "games", + "type_info": "VarcharArray" + }, + { + "ordinal": 21, + "name": "gallery", + "type_info": "VarcharArray" + }, + { + "ordinal": 22, + "name": "featured_gallery", + "type_info": "VarcharArray" + }, + { + "ordinal": 23, + "name": "version_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 24, + "name": "loader_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 25, + "name": "loader_field_enum_values", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "TextArray", + "TextArray", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + false, + true, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e" +} diff --git a/migrations/20241110010322_adds_game_version_minmax.sql b/migrations/20241110010322_adds_game_version_minmax.sql new file mode 100644 index 00000000..1fb43824 --- /dev/null +++ b/migrations/20241110010322_adds_game_version_minmax.sql @@ -0,0 +1 @@ +UPDATE loader_fields SET min_val = 1 WHERE field = 'game_versions'; \ No newline at end of file diff --git a/src/auth/oauth/mod.rs b/src/auth/oauth/mod.rs index 51a26cc6..4b989458 100644 --- a/src/auth/oauth/mod.rs +++ b/src/auth/oauth/mod.rs @@ -15,7 +15,7 @@ use crate::models::ids::OAuthClientId; use crate::models::pats::Scopes; use crate::queue::session::AuthQueue; use actix_web::http::header::LOCATION; -use actix_web::web::{scope, Data, Query, ServiceConfig}; +use actix_web::web::{Data, Query, ServiceConfig}; use actix_web::{get, post, web, HttpRequest, HttpResponse}; use chrono::Duration; use rand::distributions::Alphanumeric; @@ -33,13 +33,10 @@ pub mod errors; pub mod uris; pub fn config(cfg: &mut ServiceConfig) { - cfg.service( - scope("auth/oauth") - .service(init_oauth) - .service(accept_client_scopes) - .service(reject_client_scopes) - .service(request_token), - ); + cfg.service(init_oauth) + .service(accept_client_scopes) + .service(reject_client_scopes) + .service(request_token); } #[derive(Serialize, Deserialize)] diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index ca890b26..a4b101c3 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -295,60 +295,97 @@ pub struct SideType { impl LoaderField { pub async fn get_field<'a, E>( field: &str, + loader_ids: &[LoaderId], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let fields = Self::get_fields(exec, redis).await?; + let fields = Self::get_fields(loader_ids, exec, redis).await?; Ok(fields.into_iter().find(|f| f.field == field)) } - // Gets all fields for a given loader + // Gets all fields for a given loader(s) // Returns all as this there are probably relatively few fields per loader - // TODO: in the future, this should be to get all fields in relation to something - // - e.g. get all fields for a given game? pub async fn get_fields<'a, E>( + loader_ids: &[LoaderId], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let cached_fields = redis - .get_deserialized_from_json(LOADER_FIELDS_NAMESPACE, 0) // 0 => whatever we search for fields by - .await?; - if let Some(cached_fields) = cached_fields { - return Ok(cached_fields); - } + type RedisLoaderFieldTuple = (LoaderId, Vec); - let result = sqlx::query!( - " - SELECT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type - FROM loader_fields lf - ", - ) - .fetch_many(exec) - .try_filter_map(|e| async { - Ok(e.right().and_then(|r| { - Some(LoaderField { - id: LoaderFieldId(r.id), - field_type: LoaderFieldType::build(&r.field_type, r.enum_type)?, - field: r.field, - optional: r.optional, - min_val: r.min_val, - max_val: r.max_val, - }) - })) - }) - .try_collect::>() - .await?; + let mut loader_ids = loader_ids.to_vec(); + let cached_fields: Vec = redis + .multi_get::(LOADER_FIELDS_NAMESPACE, loader_ids.iter().map(|x| x.0)) + .await? + .into_iter() + .flatten() + .filter_map(|x: String| serde_json::from_str::(&x).ok()) + .collect(); - redis - .set_serialized_to_json(LOADER_FIELDS_NAMESPACE, &0, &result, None) + let mut found_loader_fields = vec![]; + if !cached_fields.is_empty() { + for (loader_id, fields) in cached_fields { + if loader_ids.contains(&loader_id) { + found_loader_fields.extend(fields); + loader_ids.retain(|x| x != &loader_id); + } + } + } + + if !loader_ids.is_empty() { + let result = sqlx::query!( + " + SELECT DISTINCT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type, lfl.loader_id + FROM loader_fields lf + LEFT JOIN loader_fields_loaders lfl ON lfl.loader_field_id = lf.id + WHERE lfl.loader_id = ANY($1) + ", + &loader_ids.iter().map(|x| x.0).collect::>() + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().and_then(|r| { + Some((LoaderId(r.loader_id) ,LoaderField { + id: LoaderFieldId(r.id), + field_type: LoaderFieldType::build(&r.field_type, r.enum_type)?, + field: r.field, + optional: r.optional, + min_val: r.min_val, + max_val: r.max_val, + })) + })) + }) + .try_collect::>() .await?; + let result: Vec = result + .into_iter() + .fold( + HashMap::new(), + |mut acc: HashMap>, x| { + acc.entry(x.0).or_default().push(x.1); + acc + }, + ) + .into_iter() + .collect_vec(); + + for (k, v) in result.into_iter() { + redis + .set_serialized_to_json(LOADER_FIELDS_NAMESPACE, k.0, (k, &v), None) + .await?; + found_loader_fields.extend(v); + } + } + let result = found_loader_fields + .into_iter() + .unique_by(|x| x.id) + .collect(); Ok(result) } } @@ -641,6 +678,41 @@ impl VersionField { enum_variants: Vec, ) -> Result { let value = VersionFieldValue::parse(&loader_field, value, enum_variants)?; + + // Ensure, if applicable, that the value is within the min/max bounds + let countable = match &value { + VersionFieldValue::Integer(i) => Some(*i), + VersionFieldValue::ArrayInteger(v) => Some(v.len() as i32), + VersionFieldValue::Text(_) => None, + VersionFieldValue::ArrayText(v) => Some(v.len() as i32), + VersionFieldValue::Boolean(_) => None, + VersionFieldValue::ArrayBoolean(v) => Some(v.len() as i32), + VersionFieldValue::Enum(_, _) => None, + VersionFieldValue::ArrayEnum(_, v) => Some(v.len() as i32), + }; + + if let Some(count) = countable { + if let Some(min) = loader_field.min_val { + if count < min { + return Err(format!( + "Provided value '{v}' for {field_name} is less than the minimum of {min}", + v = serde_json::to_string(&value).unwrap_or_default(), + field_name = loader_field.field, + )); + } + } + + if let Some(max) = loader_field.max_val { + if count > max { + return Err(format!( + "Provided value '{v}' for {field_name} is greater than the maximum of {max}", + v = serde_json::to_string(&value).unwrap_or_default(), + field_name = loader_field.field, + )); + } + } + } + Ok(VersionField { version_id, field_id: loader_field.id, diff --git a/src/models/v3/teams.rs b/src/models/v3/teams.rs index a77281f6..ad3c6e70 100644 --- a/src/models/v3/teams.rs +++ b/src/models/v3/teams.rs @@ -42,7 +42,7 @@ bitflags_serde_impl!(ProjectPermissions, u64); impl Default for ProjectPermissions { fn default() -> ProjectPermissions { - ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION + ProjectPermissions::empty() } } diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 1dcb5f75..9a22fd62 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -26,6 +26,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .wrap(default_cors()) .configure(admin::config) .configure(analytics_get::config) + // Todo: separate these- they need to also follow v2-v3 conversion .configure(crate::auth::session::config) .configure(crate::auth::flows::config) .configure(crate::auth::pats::config) diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index f85f785c..8755b95c 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -67,13 +67,15 @@ pub async fn project_search( facet .into_iter() .map(|facet| { - let version = match facet.split(':').nth(1) { - Some(version) => version, + let val = match facet.split(':').nth(1) { + Some(val) => val, None => return facet.to_string(), }; if facet.starts_with("versions:") { - format!("game_versions:{}", version) + format!("game_versions:{}", val) + } else if facet.starts_with("project_type:") { + format!("project_types:{}", val) } else { facet.to_string() } diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 5652e829..3e1de740 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -95,6 +95,11 @@ pub async fn version_create( json!(legacy_create.game_versions), ); + // TODO: will be overhauled with side-types overhaul + // TODO: if not, should default to previous version + fields.insert("client_side".to_string(), json!("required")); + fields.insert("server_side".to_string(), json!("optional")); + // TODO: Some kind of handling here to ensure project type is fine. // We expect the version uploaded to be of loader type modpack, but there might not be a way to check here for that. // After all, theoretically, they could be creating a genuine 'fabric' mod, and modpack no longer carries information on whether its a mod or modpack, diff --git a/src/routes/v3/admin.rs b/src/routes/v3/admin.rs new file mode 100644 index 00000000..be914e91 --- /dev/null +++ b/src/routes/v3/admin.rs @@ -0,0 +1,322 @@ +use crate::auth::validate::get_user_record_from_bearer_token; +use crate::database::models::User; +use crate::database::redis::RedisPool; +use crate::models::analytics::Download; +use crate::models::ids::ProjectId; +use crate::models::pats::Scopes; +use crate::models::users::{PayoutStatus, RecipientStatus}; +use crate::queue::analytics::AnalyticsQueue; +use crate::queue::maxmind::MaxMindIndexer; +use crate::queue::session::AuthQueue; +use crate::routes::ApiError; +use crate::search::SearchConfig; +use crate::util::date::get_current_tenths_of_ms; +use crate::util::guards::admin_key_guard; +use crate::util::routes::read_from_payload; +use actix_web::{patch, post, web, HttpRequest, HttpResponse}; +use hex::ToHex; +use hmac::{Hmac, Mac, NewMac}; +use serde::Deserialize; +use sha2::Sha256; +use sqlx::PgPool; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use uuid::Uuid; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("admin") + .service(count_download) + .service(trolley_webhook) + .service(force_reindex), + ); +} + +#[derive(Deserialize)] +pub struct DownloadBody { + pub url: String, + pub project_id: ProjectId, + pub version_name: String, + + pub ip: String, + pub headers: HashMap, +} + +// This is an internal route, cannot be used without key +#[patch("/_count-download", guard = "admin_key_guard")] +#[allow(clippy::too_many_arguments)] +pub async fn count_download( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + maxmind: web::Data>, + analytics_queue: web::Data>, + session_queue: web::Data, + download_body: web::Json, +) -> Result { + let token = download_body + .headers + .iter() + .find(|x| x.0.to_lowercase() == "authorization") + .map(|x| &**x.1); + + let user = get_user_record_from_bearer_token(&req, token, &**pool, &redis, &session_queue) + .await + .ok() + .flatten(); + + let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into(); + + let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name) + .ok() + .map(|x| x as i64); + + let (version_id, project_id) = if let Some(version) = sqlx::query!( + " + SELECT v.id id, v.mod_id mod_id FROM files f + INNER JOIN versions v ON v.id = f.version_id + WHERE f.url = $1 + ", + download_body.url, + ) + .fetch_optional(pool.as_ref()) + .await? + { + (version.id, version.mod_id) + } else if let Some(version) = sqlx::query!( + " + SELECT id, mod_id FROM versions + WHERE ((version_number = $1 OR id = $3) AND mod_id = $2) + ", + download_body.version_name, + project_id as crate::database::models::ids::ProjectId, + id_option + ) + .fetch_optional(pool.as_ref()) + .await? + { + (version.id, version.mod_id) + } else { + return Err(ApiError::InvalidInput( + "Specified version does not exist!".to_string(), + )); + }; + + let url = url::Url::parse(&download_body.url) + .map_err(|_| ApiError::InvalidInput("invalid download URL specified!".to_string()))?; + + let ip = crate::routes::analytics::convert_to_ip_v6(&download_body.ip) + .unwrap_or_else(|_| Ipv4Addr::new(127, 0, 0, 1).to_ipv6_mapped()); + + analytics_queue.add_download(Download { + id: Uuid::new_v4(), + recorded: get_current_tenths_of_ms(), + domain: url.host_str().unwrap_or_default().to_string(), + site_path: url.path().to_string(), + user_id: user + .and_then(|(scopes, x)| { + if scopes.contains(Scopes::PERFORM_ANALYTICS) { + Some(x.id.0 as u64) + } else { + None + } + }) + .unwrap_or(0), + project_id: project_id as u64, + version_id: version_id as u64, + ip, + country: maxmind.query(ip).await.unwrap_or_default(), + user_agent: download_body + .headers + .get("user-agent") + .cloned() + .unwrap_or_default(), + headers: download_body + .headers + .clone() + .into_iter() + .filter(|x| !crate::routes::analytics::FILTERED_HEADERS.contains(&&*x.0.to_lowercase())) + .collect(), + }); + + Ok(HttpResponse::NoContent().body("")) +} + +#[derive(Deserialize)] +pub struct TrolleyWebhook { + model: String, + action: String, + body: HashMap, +} + +#[post("/_trolley")] +#[allow(clippy::too_many_arguments)] +pub async fn trolley_webhook( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + mut payload: web::Payload, +) -> Result { + if let Some(signature) = req.headers().get("X-PaymentRails-Signature") { + let payload = read_from_payload( + &mut payload, + 1 << 20, + "Webhook payload exceeds the maximum of 1MiB.", + ) + .await?; + + let mut signature = signature.to_str().ok().unwrap_or_default().split(','); + let timestamp = signature + .next() + .and_then(|x| x.split('=').nth(1)) + .unwrap_or_default(); + let v1 = signature + .next() + .and_then(|x| x.split('=').nth(1)) + .unwrap_or_default(); + + let mut mac: Hmac = + Hmac::new_from_slice(dotenvy::var("TROLLEY_WEBHOOK_SIGNATURE")?.as_bytes()) + .map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?; + mac.update(timestamp.as_bytes()); + mac.update(&payload); + let request_signature = mac.finalize().into_bytes().encode_hex::(); + + if &*request_signature == v1 { + let webhook = serde_json::from_slice::(&payload)?; + + if webhook.model == "recipient" { + #[derive(Deserialize)] + struct Recipient { + pub id: String, + pub email: Option, + pub status: Option, + } + + if let Some(body) = webhook.body.get("recipient") { + if let Ok(recipient) = serde_json::from_value::(body.clone()) { + let value = sqlx::query!( + "SELECT id FROM users WHERE trolley_id = $1", + recipient.id + ) + .fetch_optional(&**pool) + .await?; + + if let Some(user) = value { + let user = User::get_id( + crate::database::models::UserId(user.id), + &**pool, + &redis, + ) + .await?; + + if let Some(user) = user { + let mut transaction = pool.begin().await?; + + if webhook.action == "deleted" { + sqlx::query!( + " + UPDATE users + SET trolley_account_status = NULL, trolley_id = NULL + WHERE id = $1 + ", + user.id.0 + ) + .execute(&mut *transaction) + .await?; + } else { + sqlx::query!( + " + UPDATE users + SET email = $1, email_verified = $2, trolley_account_status = $3 + WHERE id = $4 + ", + recipient.email.clone(), + user.email_verified && recipient.email == user.email, + recipient.status.map(|x| x.as_str()), + user.id.0 + ) + .execute(&mut *transaction).await?; + } + + transaction.commit().await?; + User::clear_caches(&[(user.id, None)], &redis).await?; + } + } + } + } + } + + if webhook.model == "payment" { + #[derive(Deserialize)] + struct Payment { + pub id: String, + pub status: PayoutStatus, + } + + if let Some(body) = webhook.body.get("payment") { + if let Ok(payment) = serde_json::from_value::(body.clone()) { + let value = sqlx::query!( + "SELECT id, amount, user_id, status FROM historical_payouts WHERE payment_id = $1", + payment.id + ) + .fetch_optional(&**pool) + .await?; + + if let Some(payout) = value { + let mut transaction = pool.begin().await?; + + if payment.status.is_failed() + && !PayoutStatus::from_string(&payout.status).is_failed() + { + sqlx::query!( + " + UPDATE users + SET balance = balance + $1 + WHERE id = $2 + ", + payout.amount, + payout.user_id, + ) + .execute(&mut *transaction) + .await?; + } + + sqlx::query!( + " + UPDATE historical_payouts + SET status = $1 + WHERE payment_id = $2 + ", + payment.status.as_str(), + payment.id, + ) + .execute(&mut *transaction) + .await?; + + transaction.commit().await?; + User::clear_caches( + &[(crate::database::models::UserId(payout.user_id), None)], + &redis, + ) + .await?; + } + } + } + } + } + } + + Ok(HttpResponse::NoContent().finish()) +} + +#[post("/_force_reindex", guard = "admin_key_guard")] +pub async fn force_reindex( + pool: web::Data, + config: web::Data, +) -> Result { + use crate::search::indexing::index_projects; + index_projects(pool.as_ref().clone(), &config).await?; + Ok(HttpResponse::NoContent().finish()) +} diff --git a/src/routes/v3/mod.rs b/src/routes/v3/mod.rs index c715d85b..7616d095 100644 --- a/src/routes/v3/mod.rs +++ b/src/routes/v3/mod.rs @@ -1,8 +1,9 @@ pub use super::ApiError; -use crate::{auth::oauth, util::cors::default_cors}; +use crate::util::cors::default_cors; use actix_web::{web, HttpResponse}; use serde_json::json; +pub mod admin; pub mod analytics_get; pub mod collections; pub mod images; @@ -27,20 +28,28 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("v3") .wrap(default_cors()) + .configure(admin::config) .configure(analytics_get::config) + // TODO: write tests that catch these + .configure(oauth_clients::config) + .configure(crate::auth::session::config) + .configure(crate::auth::flows::config) + .configure(crate::auth::pats::config) .configure(collections::config) .configure(images::config) + .configure(moderation::config) + .configure(notifications::config) .configure(organizations::config) .configure(project_creation::config) .configure(projects::config) .configure(reports::config) + .configure(statistics::config) .configure(tags::config) .configure(teams::config) .configure(threads::config) + .configure(users::config) .configure(version_file::config) - .configure(versions::config) - .configure(oauth::config) - .configure(oauth_clients::config), + .configure(versions::config), ); } diff --git a/src/routes/v3/oauth_clients.rs b/src/routes/v3/oauth_clients.rs index 0378a708..277ff912 100644 --- a/src/routes/v3/oauth_clients.rs +++ b/src/routes/v3/oauth_clients.rs @@ -43,20 +43,19 @@ use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient; use crate::models::ids::OAuthClientId as ApiOAuthClientId; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(get_user_clients); cfg.service( scope("oauth") + .configure(crate::auth::oauth::config) + .service(revoke_oauth_authorization) .service(oauth_client_create) .service(oauth_client_edit) .service(oauth_client_delete) .service(get_client) .service(get_clients) - .service(get_user_oauth_authorizations) - .service(revoke_oauth_authorization), + .service(get_user_oauth_authorizations), ); } -#[get("user/{user_id}/oauth_apps")] pub async fn get_user_clients( req: HttpRequest, info: web::Path, @@ -354,6 +353,7 @@ pub async fn revoke_oauth_authorization( redis: web::Data, session_queue: web::Data, ) -> Result { + println!("Inside revoke_oauth_authorization"); let current_user = get_user_from_headers( &req, &**pool, diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index a61e33b4..d48cd2a9 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -23,15 +23,17 @@ use sqlx::PgPool; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { + cfg.route("organizations", web::get().to(organizations_get)); cfg.service( web::scope("organization") + .route("", web::post().to(organization_create)) .route("{id}/projects", web::get().to(organization_projects_get)) .route("{id}", web::get().to(organization_get)) .route("{id}", web::patch().to(organizations_edit)) .route("{id}", web::delete().to(organization_delete)) .route("{id}/projects", web::post().to(organization_projects_add)) .route( - "{id}/projects", + "{id}/projects/{project_id}", web::delete().to(organization_projects_remove), ) .route("{id}/icon", web::patch().to(organization_icon_edit)) diff --git a/src/routes/v3/project_creation.rs b/src/routes/v3/project_creation.rs index a762b168..584225f4 100644 --- a/src/routes/v3/project_creation.rs +++ b/src/routes/v3/project_creation.rs @@ -1,8 +1,6 @@ -use super::version_creation::InitialVersionData; +use super::version_creation::{try_create_version_fields, InitialVersionData}; use crate::auth::{get_user_from_headers, AuthenticationError}; -use crate::database::models::loader_fields::{ - Loader, LoaderField, LoaderFieldEnumValue, VersionField, -}; +use crate::database::models::loader_fields::{Loader, LoaderField, LoaderFieldEnumValue}; use crate::database::models::thread_item::ThreadBuilder; use crate::database::models::{self, image_item, User}; use crate::database::redis::RedisPool; @@ -37,7 +35,7 @@ use thiserror::Error; use validator::Validate; pub fn config(cfg: &mut actix_web::web::ServiceConfig) { - cfg.route("create", web::post().to(project_create)); + cfg.route("project", web::post().to(project_create)); } #[derive(Error, Debug)] @@ -884,31 +882,19 @@ async fn create_initial_version( }) .collect::, CreateError>>()?; - let loader_fields = LoaderField::get_fields(&mut **transaction, redis).await?; - let mut version_fields = vec![]; + let loader_fields = LoaderField::get_fields(&loaders, &mut **transaction, redis).await?; let mut loader_field_enum_values = LoaderFieldEnumValue::list_many_loader_fields(&loader_fields, &mut **transaction, redis) .await?; - for (key, value) in version_data.fields.iter() { - let loader_field = loader_fields - .iter() - .find(|lf| &lf.field == key) - .ok_or_else(|| { - CreateError::InvalidInput(format!("Loader field '{key}' does not exist!")) - })?; - let enum_variants = loader_field_enum_values - .remove(&loader_field.id) - .unwrap_or_default(); - let vf: VersionField = VersionField::check_parse( - version_id.into(), - loader_field.clone(), - value.clone(), - enum_variants, - ) - .map_err(CreateError::InvalidInput)?; - version_fields.push(vf); - } + let version_fields = try_create_version_fields( + version_id, + &version_data.fields, + &loader_fields, + &mut loader_field_enum_values, + )?; + + println!("Made it past here"); let dependencies = version_data .dependencies .iter() diff --git a/src/routes/v3/projects.rs b/src/routes/v3/projects.rs index d35182aa..6106f661 100644 --- a/src/routes/v3/projects.rs +++ b/src/routes/v3/projects.rs @@ -43,7 +43,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("project") .route("{id}", web::get().to(project_get)) .route("{id}/check", web::get().to(project_get_check)) - .route("{id}", web::delete().to(project_get)) + .route("{id}", web::delete().to(project_delete)) .route("{id}", web::patch().to(project_edit)) .route("{id}/icon", web::patch().to(project_icon_edit)) .route("{id}/icon", web::delete().to(delete_project_icon)) @@ -59,7 +59,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { "members", web::get().to(super::teams::team_members_get_project), ) - .route("versions", web::get().to(super::versions::version_list)) + .route("version", web::get().to(super::versions::version_list)) .route( "version/{slug}", web::get().to(super::versions::version_project_get), diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index 4308c7dd..8900f9c4 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -7,6 +7,7 @@ use crate::database::models::loader_fields::{ }; use crate::database::redis::RedisPool; use actix_web::{web, HttpResponse}; +use itertools::Itertools; use serde_json::Value; use sqlx::PgPool; @@ -17,7 +18,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("loader", web::get().to(loader_list)), ) .route("games", web::get().to(games_list)) - .route("loader_fields", web::get().to(loader_fields_list)) + .route("loader_field", web::get().to(loader_fields_list)) .route("license", web::get().to(license_list)) .route("license/{id}", web::get().to(license_text)) .route("donation_platform", web::get().to(donation_platform_list)) @@ -118,14 +119,20 @@ pub async fn loader_fields_list( redis: web::Data, ) -> Result { let query = query.into_inner(); - let loader_field = LoaderField::get_field(&query.loader_field, &**pool, &redis) + let all_loader_ids = Loader::list(&**pool, &redis) .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( - "'{}' was not a valid loader field.", - query.loader_field - )) - })?; + .into_iter() + .map(|x| x.id) + .collect_vec(); + let loader_field = + LoaderField::get_field(&query.loader_field, &all_loader_ids, &**pool, &redis) + .await? + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "'{}' was not a valid loader field.", + query.loader_field + )) + })?; let loader_field_enum_id = match loader_field.field_type { LoaderFieldType::Enum(enum_id) | LoaderFieldType::ArrayEnum(enum_id) => enum_id, diff --git a/src/routes/v3/threads.rs b/src/routes/v3/threads.rs index 9f10c2b5..aab83aed 100644 --- a/src/routes/v3/threads.rs +++ b/src/routes/v3/threads.rs @@ -24,8 +24,8 @@ use sqlx::PgPool; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("thread") - .route("{id}", web::get().to(thread_get)) .route("inbox", web::get().to(moderation_inbox)) + .route("{id}", web::get().to(thread_get)) .route("{id}", web::post().to(thread_send_message)) .route("{id}/read", web::post().to(thread_read)), ); @@ -517,7 +517,6 @@ pub async fn moderation_inbox( Some(&[Scopes::THREAD_READ]), ) .await?; - let ids = sqlx::query!( " SELECT id diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 96b666dd..093cc1df 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -26,7 +26,7 @@ use crate::{ util::{routes::read_from_payload, validate::validation_errors_to_string}, }; -use super::ApiError; +use super::{oauth_clients::get_user_clients, ApiError}; pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("user", web::get().to(user_auth_get)); @@ -45,7 +45,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("{id}/notifications", web::get().to(user_notifications)) .route("{id}/payouts", web::get().to(user_payouts)) .route("{id}/payouts_fees", web::get().to(user_payouts_fees)) - .route("{id}/payouts", web::post().to(user_payouts_request)), + .route("{id}/payouts", web::post().to(user_payouts_request)) + .route("{id}/oauth_apps", web::get().to(get_user_clients)), ); } diff --git a/src/routes/v3/version_creation.rs b/src/routes/v3/version_creation.rs index 106b956d..9caad08f 100644 --- a/src/routes/v3/version_creation.rs +++ b/src/routes/v3/version_creation.rs @@ -30,7 +30,7 @@ use futures::stream::StreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use validator::Validate; @@ -256,37 +256,6 @@ async fn version_create_inner( let all_loaders = models::loader_fields::Loader::list(&mut **transaction, redis).await?; - - let loader_fields = LoaderField::get_fields(&mut **transaction, redis).await?; - let mut version_fields = vec![]; - let mut loader_field_enum_values = LoaderFieldEnumValue::list_many_loader_fields( - &loader_fields, - &mut **transaction, - redis, - ) - .await?; - for (key, value) in version_create_data.fields.iter() { - let loader_field = loader_fields - .iter() - .find(|lf| &lf.field == key) - .ok_or_else(|| { - CreateError::InvalidInput(format!( - "Loader field '{key}' does not exist!" - )) - })?; - let enum_variants = loader_field_enum_values - .remove(&loader_field.id) - .unwrap_or_default(); - let vf: VersionField = VersionField::check_parse( - version_id.into(), - loader_field.clone(), - value.clone(), - enum_variants, - ) - .map_err(CreateError::InvalidInput)?; - version_fields.push(vf); - } - let loaders = version_create_data .loaders .iter() @@ -299,7 +268,22 @@ async fn version_create_inner( }) .collect::, _>>()?; selected_loaders = Some(loaders.clone()); - let loader_ids = loaders.iter().map(|y| y.id).collect_vec(); + let loader_ids: Vec = loaders.iter().map(|y| y.id).collect_vec(); + + let loader_fields = + LoaderField::get_fields(&loader_ids, &mut **transaction, redis).await?; + let mut loader_field_enum_values = LoaderFieldEnumValue::list_many_loader_fields( + &loader_fields, + &mut **transaction, + redis, + ) + .await?; + let version_fields = try_create_version_fields( + version_id, + &version_create_data.fields, + &loader_fields, + &mut loader_field_enum_values, + )?; let dependencies = version_create_data .dependencies @@ -966,3 +950,50 @@ pub fn get_name_ext( }; Ok((file_name, file_extension)) } + +// Reused functionality between project_creation and version_creation +// Create a list of VersionFields from the fetched data, and check that all mandatory fields are present +pub fn try_create_version_fields( + version_id: VersionId, + submitted_fields: &HashMap, + loader_fields: &[LoaderField], + loader_field_enum_values: &mut HashMap>, +) -> Result, CreateError> { + let mut version_fields = vec![]; + let mut remaining_mandatory_loader_fields = loader_fields + .iter() + .filter(|lf| !lf.optional) + .map(|lf| lf.field.clone()) + .collect::>(); + for (key, value) in submitted_fields.iter() { + let loader_field = loader_fields + .iter() + .find(|lf| &lf.field == key) + .ok_or_else(|| { + CreateError::InvalidInput(format!( + "Loader field '{key}' does not exist for any loaders supplied," + )) + })?; + remaining_mandatory_loader_fields.remove(&loader_field.field); + let enum_variants = loader_field_enum_values + .remove(&loader_field.id) + .unwrap_or_default(); + + let vf: VersionField = VersionField::check_parse( + version_id.into(), + loader_field.clone(), + value.clone(), + enum_variants, + ) + .map_err(CreateError::InvalidInput)?; + version_fields.push(vf); + } + + if !remaining_mandatory_loader_fields.is_empty() { + return Err(CreateError::InvalidInput(format!( + "Missing mandatory loader fields: {}", + remaining_mandatory_loader_fields.iter().join(", ") + ))); + } + Ok(version_fields) +} diff --git a/src/routes/v3/version_file.rs b/src/routes/v3/version_file.rs index 558cf5f9..4820362f 100644 --- a/src/routes/v3/version_file.rs +++ b/src/routes/v3/version_file.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("version_file") - .route("version_id", web::get().to(get_version_from_hash)) + .route("{version_id}", web::get().to(get_version_from_hash)) .route("{version_id}/update", web::post().to(get_update_from_hash)) .route("project", web::post().to(get_projects_from_hashes)) .route("{version_id}", web::delete().to(delete_file)) @@ -380,7 +380,7 @@ pub async fn update_files( Ok(HttpResponse::Ok().json(response)) } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct FileUpdateData { pub hash: String, pub loaders: Option>, @@ -388,7 +388,7 @@ pub struct FileUpdateData { pub version_types: Option>, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct ManyFileUpdateData { #[serde(default = "default_algorithm")] pub algorithm: String, @@ -461,6 +461,7 @@ pub async fn update_individual_files( if let Some(loaders) = &query_file.loaders { bool &= x.loaders.iter().any(|y| loaders.contains(y)); } + if let Some(loader_fields) = &query_file.loader_fields { for (key, values) in loader_fields { bool &= if let Some(x_vf) = @@ -472,7 +473,6 @@ pub async fn update_individual_files( }; } } - bool }) .sorted() diff --git a/src/routes/v3/versions.rs b/src/routes/v3/versions.rs index b3504ee9..1e5a7f14 100644 --- a/src/routes/v3/versions.rs +++ b/src/routes/v3/versions.rs @@ -5,7 +5,9 @@ use crate::auth::{ filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version, }; use crate::database; -use crate::database::models::loader_fields::{LoaderField, LoaderFieldEnumValue, VersionField}; +use crate::database::models::loader_fields::{ + self, LoaderField, LoaderFieldEnumValue, VersionField, +}; use crate::database::models::version_item::{DependencyBuilder, LoaderVersion}; use crate::database::models::{image_item, Organization}; use crate::database::redis::RedisPool; @@ -22,6 +24,7 @@ use crate::util::img; use crate::util::validate::validation_errors_to_string; use actix_web::{web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; @@ -384,7 +387,14 @@ pub async fn version_edit_helper( .map(|x| x.to_string()) .collect::>(); - let loader_fields = LoaderField::get_fields(&mut *transaction, &redis) + let all_loaders = loader_fields::Loader::list(&mut *transaction, &redis).await?; + let loader_ids = version_item + .loaders + .iter() + .filter_map(|x| all_loaders.iter().find(|y| &y.loader == x).map(|y| y.id)) + .collect_vec(); + + let loader_fields = LoaderField::get_fields(&loader_ids, &mut *transaction, &redis) .await? .into_iter() .filter(|lf| version_fields_names.contains(&lf.field)) @@ -417,7 +427,7 @@ pub async fn version_edit_helper( .find(|lf| lf.field == vf_name) .ok_or_else(|| { ApiError::InvalidInput(format!( - "Loader field '{vf_name}' does not exist." + "Loader field '{vf_name}' does not exist for any loaders supplied." )) })?; let enum_variants = loader_field_enum_values diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index b2bf89af..6ff4d6db 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -22,7 +22,7 @@ pub async fn index_local( SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.published published, m.approved approved, m.updated updated, m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color, - pt.name project_type_name, u.username username, + u.username username, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders, @@ -79,7 +79,7 @@ pub async fn index_local( LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id WHERE v.status != ANY($1) - GROUP BY v.id, m.id, pt.id, u.id; + GROUP BY v.id, m.id, u.id; ", &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::>(), @@ -146,7 +146,7 @@ pub async fn index_local( modified_timestamp: m.updated.timestamp(), license, slug: m.slug, - project_type: m.project_type_name, + project_types: m.project_types.unwrap_or_default(), gallery: m.gallery.unwrap_or_default(), display_categories, open_source, diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index e6e1f378..97155916 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -176,7 +176,7 @@ fn default_settings() -> Settings { const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "project_id", "version_id", - "project_type", + "project_types", "slug", "author", "title", @@ -200,7 +200,7 @@ const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["title", "description", "author const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "categories", "license", - "project_type", + "project_types", "downloads", "follows", "author", diff --git a/src/search/mod.rs b/src/search/mod.rs index becf32aa..4c5648a7 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -74,7 +74,7 @@ impl SearchConfig { pub struct UploadSearchProject { pub version_id: String, pub project_id: String, - pub project_type: String, + pub project_types: Vec, pub slug: Option, pub author: String, pub title: String, @@ -114,7 +114,7 @@ pub struct SearchResults { pub struct ResultSearchProject { pub version_id: String, pub project_id: String, - pub project_type: String, + pub project_types: Vec, pub slug: Option, pub author: String, pub title: String, diff --git a/src/util/webhook.rs b/src/util/webhook.rs index 0e5f6ad2..11039949 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -87,7 +87,7 @@ pub async fn send_discord_webhook( " SELECT m.id id, m.title title, m.description description, m.color color, m.icon_url icon_url, m.slug slug, - pt.name project_type, u.username username, u.avatar_url avatar_url, + u.username username, u.avatar_url avatar_url, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories, ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, @@ -142,7 +142,7 @@ pub async fn send_discord_webhook( LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id WHERE m.id = $1 - GROUP BY m.id, pt.id, u.id; + GROUP BY m.id, u.id; ", project_id.0 as i64, &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), @@ -246,7 +246,8 @@ pub async fn send_discord_webhook( }); } - let mut project_type = project.project_type; + let mut project_types: Vec = project.project_types.unwrap_or_default(); + let mut project_type = project_types.pop().unwrap_or_default(); // TODO: Should this grab a not-first? if loaders.iter().all(|x| PLUGIN_LOADERS.contains(&&**x)) { project_type = "plugin".to_string(); diff --git a/tests/analytics.rs b/tests/analytics.rs index e762566f..bc3d80d4 100644 --- a/tests/analytics.rs +++ b/tests/analytics.rs @@ -1,18 +1,16 @@ use chrono::{DateTime, Duration, Utc}; use common::database::*; +use common::environment::TestEnvironment; use itertools::Itertools; use labrinth::models::ids::base62_impl::parse_base62; use rust_decimal::{prelude::ToPrimitive, Decimal}; -use crate::common::environment::TestEnvironment; - -// importing common module. mod common; #[actix_rt::test] pub async fn analytics_revenue() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id = test_env .dummy diff --git a/tests/common/api_v2/mod.rs b/tests/common/api_v2/mod.rs index 2a85bbb3..8f5f470f 100644 --- a/tests/common/api_v2/mod.rs +++ b/tests/common/api_v2/mod.rs @@ -4,8 +4,8 @@ use super::environment::LocalService; use actix_web::dev::ServiceResponse; use std::rc::Rc; -pub mod organization; pub mod project; +pub mod request_data; pub mod tags; pub mod team; pub mod version; diff --git a/tests/common/api_v2/project.rs b/tests/common/api_v2/project.rs index 25075089..10f910d2 100644 --- a/tests/common/api_v2/project.rs +++ b/tests/common/api_v2/project.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; - +use crate::common::api_v2::request_data::ProjectCreationRequestData; use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, @@ -14,14 +13,11 @@ use labrinth::{ }; use rust_decimal::Decimal; use serde_json::json; +use std::collections::HashMap; -use crate::common::{ - asserts::assert_status, - database::MOD_USER_PAT, - request_data::{ImageData, ProjectCreationRequestData}, -}; +use crate::common::{asserts::assert_status, database::MOD_USER_PAT}; -use super::ApiV2; +use super::{request_data::ImageData, ApiV2}; impl ApiV2 { pub async fn add_public_project( diff --git a/tests/common/request_data.rs b/tests/common/api_v2/request_data.rs similarity index 98% rename from tests/common/request_data.rs rename to tests/common/api_v2/request_data.rs index 1522ded2..a7a84c30 100644 --- a/tests/common/request_data.rs +++ b/tests/common/api_v2/request_data.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use serde_json::json; -use super::dummy_data::{DummyImage, TestFile}; +use crate::common::dummy_data::{DummyImage, TestFile}; use labrinth::{ models::projects::ProjectId, util::actix::{MultipartSegment, MultipartSegmentData}, diff --git a/tests/common/api_v2/version.rs b/tests/common/api_v2/version.rs index eafef956..935a5b7f 100644 --- a/tests/common/api_v2/version.rs +++ b/tests/common/api_v2/version.rs @@ -12,9 +12,9 @@ use labrinth::{ }; use serde_json::json; -use crate::common::{asserts::assert_status, request_data::VersionCreationRequestData}; +use crate::common::asserts::assert_status; -use super::ApiV2; +use super::{request_data::VersionCreationRequestData, ApiV2}; pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { let serialized = serde_json::to_string(&elements).unwrap(); @@ -327,8 +327,7 @@ impl ApiV2 { test::read_body_json(resp).await } - // TODO: remove redundancy in these functions - + // TODO: remove redundancy in these functions- some are essentially repeats pub async fn create_default_version( &self, project_id: &str, diff --git a/tests/common/api_v3/mod.rs b/tests/common/api_v3/mod.rs index e0c36798..9ed4bce2 100644 --- a/tests/common/api_v3/mod.rs +++ b/tests/common/api_v3/mod.rs @@ -6,7 +6,12 @@ use std::rc::Rc; pub mod oauth; pub mod oauth_clients; +pub mod organization; +pub mod project; +pub mod request_data; pub mod tags; +pub mod team; +pub mod version; #[derive(Clone)] pub struct ApiV3 { @@ -17,4 +22,15 @@ impl ApiV3 { pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { self.test_app.call(req).await.unwrap() } + + pub async fn reset_search_index(&self) -> ServiceResponse { + let req = actix_web::test::TestRequest::post() + .uri("/v3/admin/_force_reindex") + .append_header(( + "Modrinth-Admin", + dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(), + )) + .to_request(); + self.call(req).await + } } diff --git a/tests/common/api_v3/oauth.rs b/tests/common/api_v3/oauth.rs index 1a6b35f4..125b3dec 100644 --- a/tests/common/api_v3/oauth.rs +++ b/tests/common/api_v3/oauth.rs @@ -55,7 +55,7 @@ impl ApiV3 { pub async fn oauth_accept(&self, flow: &str, pat: &str) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/auth/oauth/accept") + .uri("/v3/oauth/accept") .append_header((AUTHORIZATION, pat)) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), @@ -68,7 +68,7 @@ impl ApiV3 { pub async fn oauth_reject(&self, flow: &str, pat: &str) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/auth/oauth/reject") + .uri("/v3/oauth/reject") .append_header((AUTHORIZATION, pat)) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), @@ -87,7 +87,7 @@ impl ApiV3 { ) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/auth/oauth/token") + .uri("/v3/oauth/token") .append_header((AUTHORIZATION, client_secret)) .set_form(TokenRequest { grant_type: "authorization_code".to_string(), @@ -108,7 +108,7 @@ pub fn generate_authorize_uri( state: Option<&str>, ) -> String { format!( - "/v3/auth/oauth/authorize?client_id={}{}{}{}", + "/v3/oauth/authorize?client_id={}{}{}{}", urlencoding::encode(client_id), optional_query_param("redirect_uri", redirect_uri), optional_query_param("scope", scope), diff --git a/tests/common/api_v2/organization.rs b/tests/common/api_v3/organization.rs similarity index 84% rename from tests/common/api_v2/organization.rs rename to tests/common/api_v3/organization.rs index 5cfb214d..4a17cd2f 100644 --- a/tests/common/api_v2/organization.rs +++ b/tests/common/api_v3/organization.rs @@ -3,14 +3,12 @@ use actix_web::{ test::{self, TestRequest}, }; use bytes::Bytes; -use labrinth::models::{organizations::Organization, v2::projects::LegacyProject}; +use labrinth::models::{organizations::Organization, v3::projects::Project}; use serde_json::json; -use crate::common::request_data::ImageData; +use super::{request_data::ImageData, ApiV3}; -use super::ApiV2; - -impl ApiV2 { +impl ApiV3 { pub async fn create_organization( &self, organization_title: &str, @@ -18,7 +16,7 @@ impl ApiV2 { pat: &str, ) -> ServiceResponse { let req = test::TestRequest::post() - .uri("/v2/organization") + .uri("/v3/organization") .append_header(("Authorization", pat)) .set_json(json!({ "title": organization_title, @@ -30,7 +28,7 @@ impl ApiV2 { pub async fn get_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() - .uri(&format!("/v2/organization/{id_or_title}")) + .uri(&format!("/v3/organization/{id_or_title}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await @@ -48,7 +46,7 @@ impl ApiV2 { pub async fn get_organization_projects(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() - .uri(&format!("/v2/organization/{id_or_title}/projects")) + .uri(&format!("/v3/organization/{id_or_title}/projects")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await @@ -58,7 +56,7 @@ impl ApiV2 { &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_organization_projects(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -71,7 +69,7 @@ impl ApiV2 { pat: &str, ) -> ServiceResponse { let req = test::TestRequest::patch() - .uri(&format!("/v2/organization/{id_or_title}")) + .uri(&format!("/v3/organization/{id_or_title}")) .append_header(("Authorization", pat)) .set_json(patch) .to_request(); @@ -89,7 +87,7 @@ impl ApiV2 { // If an icon is provided, upload it let req = test::TestRequest::patch() .uri(&format!( - "/v2/organization/{id_or_title}/icon?ext={ext}", + "/v3/organization/{id_or_title}/icon?ext={ext}", ext = icon.extension )) .append_header(("Authorization", pat)) @@ -100,7 +98,7 @@ impl ApiV2 { } else { // If no icon is provided, delete the icon let req = test::TestRequest::delete() - .uri(&format!("/v2/organization/{id_or_title}/icon")) + .uri(&format!("/v3/organization/{id_or_title}/icon")) .append_header(("Authorization", pat)) .to_request(); @@ -110,7 +108,7 @@ impl ApiV2 { pub async fn delete_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() - .uri(&format!("/v2/organization/{id_or_title}")) + .uri(&format!("/v3/organization/{id_or_title}")) .append_header(("Authorization", pat)) .to_request(); @@ -124,7 +122,7 @@ impl ApiV2 { pat: &str, ) -> ServiceResponse { let req = test::TestRequest::post() - .uri(&format!("/v2/organization/{id_or_title}/projects")) + .uri(&format!("/v3/organization/{id_or_title}/projects")) .append_header(("Authorization", pat)) .set_json(json!({ "project_id": project_id_or_slug, @@ -142,7 +140,7 @@ impl ApiV2 { ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!( - "/v2/organization/{id_or_title}/projects/{project_id_or_slug}" + "/v3/organization/{id_or_title}/projects/{project_id_or_slug}" )) .append_header(("Authorization", pat)) .to_request(); diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs new file mode 100644 index 00000000..b4365d9c --- /dev/null +++ b/tests/common/api_v3/project.rs @@ -0,0 +1,256 @@ +use std::collections::HashMap; + +use actix_http::StatusCode; +use actix_web::{ + dev::ServiceResponse, + test::{self, TestRequest}, +}; +use bytes::Bytes; +use chrono::{DateTime, Utc}; +use labrinth::{ + models::v3::projects::{Project, Version}, + search::SearchResults, + util::actix::AppendsMultipart, +}; +use rust_decimal::Decimal; +use serde_json::json; + +use crate::common::{asserts::assert_status, database::MOD_USER_PAT}; + +use super::{ + request_data::{ImageData, ProjectCreationRequestData}, + ApiV3, +}; + +impl ApiV3 { + pub async fn add_public_project( + &self, + creation_data: ProjectCreationRequestData, + pat: &str, + ) -> (Project, Vec) { + // Add a project. + let req = TestRequest::post() + .uri("/v3/project") + .append_header(("Authorization", pat)) + .set_multipart(creation_data.segment_data) + .to_request(); + let resp = self.call(req).await; + assert_status(&resp, StatusCode::OK); + + // Approve as a moderator. + let req = TestRequest::patch() + .uri(&format!("/v3/project/{}", creation_data.slug)) + .append_header(("Authorization", MOD_USER_PAT)) + .set_json(json!( + { + "status": "approved" + } + )) + .to_request(); + let resp = self.call(req).await; + assert_status(&resp, StatusCode::NO_CONTENT); + + let project = self + .get_project_deserialized(&creation_data.slug, pat) + .await; + + // Get project's versions + let req = TestRequest::get() + .uri(&format!("/v3/project/{}/version", creation_data.slug)) + .append_header(("Authorization", pat)) + .to_request(); + let resp = self.call(req).await; + let versions: Vec = test::read_body_json(resp).await; + + (project, versions) + } + + pub async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v3/project/{project_slug_or_id}")) + .append_header(("Authorization", pat)) + .to_request(); + let resp = self.call(req).await; + assert_eq!(resp.status(), 204); + resp + } + + pub async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::get() + .uri(&format!("/v3/project/{id_or_slug}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project { + let resp = self.get_project(id_or_slug, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/user/{}/projects", user_id_or_username)) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_user_projects_deserialized( + &self, + user_id_or_username: &str, + pat: &str, + ) -> Vec { + let resp = self.get_user_projects(user_id_or_username, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn edit_project( + &self, + id_or_slug: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v3/project/{id_or_slug}")) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + + self.call(req).await + } + + pub async fn edit_project_bulk( + &self, + ids_or_slugs: impl IntoIterator, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let projects_str = ids_or_slugs + .into_iter() + .map(|s| format!("\"{}\"", s)) + .collect::>() + .join(","); + let req = test::TestRequest::patch() + .uri(&format!( + "/v3/projects?ids={encoded}", + encoded = urlencoding::encode(&format!("[{projects_str}]")) + )) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + + self.call(req).await + } + + pub async fn edit_project_icon( + &self, + id_or_slug: &str, + icon: Option, + pat: &str, + ) -> ServiceResponse { + if let Some(icon) = icon { + // If an icon is provided, upload it + let req = test::TestRequest::patch() + .uri(&format!( + "/v3/project/{id_or_slug}/icon?ext={ext}", + ext = icon.extension + )) + .append_header(("Authorization", pat)) + .set_payload(Bytes::from(icon.icon)) + .to_request(); + + self.call(req).await + } else { + // If no icon is provided, delete the icon + let req = test::TestRequest::delete() + .uri(&format!("/v3/project/{id_or_slug}/icon")) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } + } + + pub async fn search_deserialized( + &self, + query: Option<&str>, + facets: Option, + pat: &str, + ) -> SearchResults { + let query_field = if let Some(query) = query { + format!("&query={}", urlencoding::encode(query)) + } else { + "".to_string() + }; + + let facets_field = if let Some(facets) = facets { + format!("&facets={}", urlencoding::encode(&facets.to_string())) + } else { + "".to_string() + }; + + let req = test::TestRequest::get() + .uri(&format!("/v3/search?{}{}", query_field, facets_field)) + .append_header(("Authorization", pat)) + .to_request(); + let resp = self.call(req).await; + let status = resp.status(); + assert_eq!(status, 200); + test::read_body_json(resp).await + } + + pub async fn get_analytics_revenue( + &self, + id_or_slugs: Vec<&str>, + start_date: Option>, + end_date: Option>, + resolution_minutes: Option, + pat: &str, + ) -> ServiceResponse { + let projects_string = serde_json::to_string(&id_or_slugs).unwrap(); + let projects_string = urlencoding::encode(&projects_string); + + let mut extra_args = String::new(); + if let Some(start_date) = start_date { + let start_date = start_date.to_rfc3339(); + // let start_date = serde_json::to_string(&start_date).unwrap(); + let start_date = urlencoding::encode(&start_date); + extra_args.push_str(&format!("&start_date={start_date}")); + } + if let Some(end_date) = end_date { + let end_date = end_date.to_rfc3339(); + // let end_date = serde_json::to_string(&end_date).unwrap(); + let end_date = urlencoding::encode(&end_date); + extra_args.push_str(&format!("&end_date={end_date}")); + } + if let Some(resolution_minutes) = resolution_minutes { + extra_args.push_str(&format!("&resolution_minutes={}", resolution_minutes)); + } + + let req = test::TestRequest::get() + .uri(&format!( + "/v3/analytics/revenue?{projects_string}{extra_args}", + )) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } + + pub async fn get_analytics_revenue_deserialized( + &self, + id_or_slugs: Vec<&str>, + start_date: Option>, + end_date: Option>, + resolution_minutes: Option, + pat: &str, + ) -> HashMap> { + let resp = self + .get_analytics_revenue(id_or_slugs, start_date, end_date, resolution_minutes, pat) + .await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} diff --git a/tests/common/api_v3/request_data.rs b/tests/common/api_v3/request_data.rs new file mode 100644 index 00000000..6091992c --- /dev/null +++ b/tests/common/api_v3/request_data.rs @@ -0,0 +1,146 @@ +#![allow(dead_code)] +use serde_json::json; + +use crate::common::dummy_data::{DummyImage, TestFile}; +use labrinth::{ + models::projects::ProjectId, + util::actix::{MultipartSegment, MultipartSegmentData}, +}; + +pub struct ProjectCreationRequestData { + pub slug: String, + pub jar: Option, + pub segment_data: Vec, +} + +pub struct VersionCreationRequestData { + pub version: String, + pub jar: Option, + pub segment_data: Vec, +} + +pub struct ImageData { + pub filename: String, + pub extension: String, + pub icon: Vec, +} + +pub fn get_public_project_creation_data( + slug: &str, + version_jar: Option, +) -> ProjectCreationRequestData { + let json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + let multipart_data = get_public_creation_data_multipart(&json_data, version_jar.as_ref()); + ProjectCreationRequestData { + slug: slug.to_string(), + jar: version_jar, + segment_data: multipart_data, + } +} + +pub fn get_public_version_creation_data( + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + // closure that takes in a &mut serde_json::Value + // and modifies it before it is serialized and sent + modify_json: Option, +) -> VersionCreationRequestData { + let mut json_data = get_public_version_creation_data_json(version_number, &version_jar); + json_data["project_id"] = json!(project_id); + if let Some(modify_json) = modify_json { + modify_json(&mut json_data); + } + + let multipart_data = get_public_creation_data_multipart(&json_data, Some(&version_jar)); + VersionCreationRequestData { + version: version_number.to_string(), + jar: Some(version_jar), + segment_data: multipart_data, + } +} + +pub fn get_public_version_creation_data_json( + version_number: &str, + version_jar: &TestFile, +) -> serde_json::Value { + let is_modpack = version_jar.project_type() == "modpack"; + let mut j = json!({ + "file_parts": [version_jar.filename()], + "version_number": version_number, + "version_title": "start", + "dependencies": [], + "release_channel": "release", + "loaders": [if is_modpack { "mrpack" } else { "fabric" }], + "featured": true, + + // Loader fields + "game_versions": ["1.20.1"], + "client_side": "required", + "server_side": "optional" + }); + if is_modpack { + j["mrpack_loaders"] = json!(["fabric"]); + } + j +} + +pub fn get_public_project_creation_data_json( + slug: &str, + version_jar: Option<&TestFile>, +) -> serde_json::Value { + let initial_versions = if let Some(jar) = version_jar { + json!([get_public_version_creation_data_json("1.2.3", jar)]) + } else { + json!([]) + }; + + let is_draft = version_jar.is_none(); + json!( + { + "title": format!("Test Project {slug}"), + "slug": slug, + "description": "A dummy project for testing with.", + "body": "This project is approved, and versions are listed.", + "initial_versions": initial_versions, + "is_draft": is_draft, + "categories": [], + "license_id": "MIT", + } + ) +} + +pub fn get_public_creation_data_multipart( + json_data: &serde_json::Value, + version_jar: Option<&TestFile>, +) -> Vec { + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(json_data).unwrap()), + }; + + if let Some(jar) = version_jar { + // Basic file + let file_segment = MultipartSegment { + name: jar.filename(), + filename: Some(jar.filename()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary(jar.bytes()), + }; + + vec![json_segment, file_segment] + } else { + vec![json_segment] + } +} + +pub fn get_icon_data(dummy_icon: DummyImage) -> ImageData { + ImageData { + filename: dummy_icon.filename(), + extension: dummy_icon.extension(), + icon: dummy_icon.bytes(), + } +} diff --git a/tests/common/api_v3/tags.rs b/tests/common/api_v3/tags.rs index dd36ad74..afd7a8ec 100644 --- a/tests/common/api_v3/tags.rs +++ b/tests/common/api_v3/tags.rs @@ -3,12 +3,61 @@ use actix_web::{ test::{self, TestRequest}, }; use labrinth::routes::v3::tags::GameData; +use labrinth::{ + database::models::loader_fields::LoaderFieldEnumValue, + routes::v3::tags::{CategoryData, LoaderData}, +}; use crate::common::database::ADMIN_USER_PAT; use super::ApiV3; impl ApiV3 { + pub async fn get_loaders(&self) -> ServiceResponse { + let req = TestRequest::get() + .uri("/v3/tag/loader") + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + self.call(req).await + } + + pub async fn get_loaders_deserialized(&self) -> Vec { + let resp = self.get_loaders().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_categories(&self) -> ServiceResponse { + let req = TestRequest::get() + .uri("/v3/tag/category") + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + self.call(req).await + } + + pub async fn get_categories_deserialized(&self) -> Vec { + let resp = self.get_categories().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_loader_field_variants(&self, loader_field: &str) -> ServiceResponse { + let req = TestRequest::get() + .uri(&format!("/v3/loader_field?loader_field={}", loader_field)) + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + self.call(req).await + } + + pub async fn get_loader_field_variants_deserialized( + &self, + loader_field: &str, + ) -> Vec { + let resp = self.get_loader_field_variants(loader_field).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + // TODO: fold this into v3 API of other v3 testing PR pub async fn get_games(&self) -> ServiceResponse { let req = TestRequest::get() diff --git a/tests/common/api_v3/team.rs b/tests/common/api_v3/team.rs new file mode 100644 index 00000000..05b6b137 --- /dev/null +++ b/tests/common/api_v3/team.rs @@ -0,0 +1,176 @@ +use actix_http::StatusCode; +use actix_web::{dev::ServiceResponse, test}; +use labrinth::models::{ + notifications::Notification, + teams::{OrganizationPermissions, ProjectPermissions, TeamMember}, +}; +use serde_json::json; + +use crate::common::asserts::assert_status; + +use super::ApiV3; + +impl ApiV3 { + pub async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/team/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_team_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_team_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_project_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_project_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/organization/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_organization_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_organization_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{team_id}/join")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn remove_from_team( + &self, + team_id: &str, + user_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v3/team/{team_id}/members/{user_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn edit_team_member( + &self, + team_id: &str, + user_id: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{team_id}/members/{user_id}")) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + self.call(req).await + } + + pub async fn transfer_team_ownership( + &self, + team_id: &str, + user_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{team_id}/owner")) + .append_header(("Authorization", pat)) + .set_json(json!({ + "user_id": user_id, + })) + .to_request(); + self.call(req).await + } + + pub async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/user/{user_id}/notifications")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_user_notifications_deserialized( + &self, + user_id: &str, + pat: &str, + ) -> Vec { + let resp = self.get_user_notifications(user_id, pat).await; + assert_status(&resp, StatusCode::OK); + test::read_body_json(resp).await + } + + pub async fn mark_notification_read( + &self, + notification_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v3/notification/{notification_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + pub async fn add_user_to_team( + &self, + team_id: &str, + user_id: &str, + project_permissions: Option, + organization_permissions: Option, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{team_id}/members")) + .append_header(("Authorization", pat)) + .set_json(json!( { + "user_id": user_id, + "permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(), + "organization_permissions" : organization_permissions.map(|p| p.bits()), + })) + .to_request(); + self.call(req).await + } + + pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v3/notification/{notification_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } +} diff --git a/tests/common/api_v3/version.rs b/tests/common/api_v3/version.rs new file mode 100644 index 00000000..b1ae5693 --- /dev/null +++ b/tests/common/api_v3/version.rs @@ -0,0 +1,422 @@ +use std::collections::HashMap; + +use actix_http::{header::AUTHORIZATION, StatusCode}; +use actix_web::{ + dev::ServiceResponse, + test::{self, TestRequest}, +}; +use labrinth::{ + models::{projects::VersionType, v3::projects::Version}, + routes::v3::version_file::FileUpdateData, + util::actix::AppendsMultipart, +}; +use serde_json::json; + +use crate::common::asserts::assert_status; + +use super::{request_data::VersionCreationRequestData, ApiV3}; + +pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { + let serialized = serde_json::to_string(&elements).unwrap(); + urlencoding::encode(&serialized).to_string() +} + +impl ApiV3 { + pub async fn add_public_version( + &self, + creation_data: VersionCreationRequestData, + pat: &str, + ) -> ServiceResponse { + // Add a project. + let req = TestRequest::post() + .uri("/v3/version") + .append_header(("Authorization", pat)) + .set_multipart(creation_data.segment_data) + .to_request(); + self.call(req).await + } + + pub async fn add_public_version_deserialized( + &self, + creation_data: VersionCreationRequestData, + pat: &str, + ) -> Version { + let resp = self.add_public_version(creation_data, pat).await; + assert_status(&resp, StatusCode::OK); + let value: serde_json::Value = test::read_body_json(resp).await; + let version_id = value["id"].as_str().unwrap(); + self.get_version_deserialized(version_id, pat).await + } + + pub async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::get() + .uri(&format!("/v3/version/{id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> Version { + let resp = self.get_version(id, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn edit_version( + &self, + version_id: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v3/version/{version_id}")) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + + self.call(req).await + } + + pub async fn get_version_from_hash( + &self, + hash: &str, + algorithm: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/version_file/{hash}?algorithm={algorithm}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_version_from_hash_deserialized( + &self, + hash: &str, + algorithm: &str, + pat: &str, + ) -> Version { + let resp = self.get_version_from_hash(hash, algorithm, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_versions_from_hashes( + &self, + hashes: &[&str], + algorithm: &str, + pat: &str, + ) -> ServiceResponse { + let req = TestRequest::post() + .uri("/v3/version_files") + .append_header(("Authorization", pat)) + .set_json(json!({ + "hashes": hashes, + "algorithm": algorithm, + })) + .to_request(); + self.call(req).await + } + + pub async fn get_versions_from_hashes_deserialized( + &self, + hashes: &[&str], + algorithm: &str, + pat: &str, + ) -> HashMap { + let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_update_from_hash( + &self, + hash: &str, + algorithm: &str, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> ServiceResponse { + let mut json = json!({}); + if let Some(loaders) = loaders { + json["loaders"] = serde_json::to_value(loaders).unwrap(); + } + if let Some(game_versions) = game_versions { + json["loader_fields"] = json!({ + "game_versions": game_versions, + }); + } + if let Some(version_types) = version_types { + json["version_types"] = serde_json::to_value(version_types).unwrap(); + } + + let req = test::TestRequest::post() + .uri(&format!( + "/v3/version_file/{hash}/update?algorithm={algorithm}" + )) + .append_header(("Authorization", pat)) + .set_json(json) + .to_request(); + self.call(req).await + } + + pub async fn get_update_from_hash_deserialized( + &self, + hash: &str, + algorithm: &str, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> Version { + let resp = self + .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) + .await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn update_files( + &self, + algorithm: &str, + hashes: Vec, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> ServiceResponse { + let mut json = json!({ + "algorithm": algorithm, + "hashes": hashes, + }); + if let Some(loaders) = loaders { + json["loaders"] = serde_json::to_value(loaders).unwrap(); + } + if let Some(game_versions) = game_versions { + json["loader_fields"] = json!({ + "game_versions": game_versions, + }); + } + if let Some(version_types) = version_types { + json["version_types"] = serde_json::to_value(version_types).unwrap(); + } + + let req = test::TestRequest::post() + .uri("/v3/version_files/update") + .append_header(("Authorization", pat)) + .set_json(json) + .to_request(); + self.call(req).await + } + + pub async fn update_files_deserialized( + &self, + algorithm: &str, + hashes: Vec, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> HashMap { + let resp = self + .update_files( + algorithm, + hashes, + loaders, + game_versions, + version_types, + pat, + ) + .await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn update_individual_files( + &self, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v3/version_files/update_individual") + .append_header(("Authorization", pat)) + .set_json(json!({ + "algorithm": algorithm, + "hashes": hashes + })) + .to_request(); + self.call(req).await + } + + pub async fn update_individual_files_deserialized( + &self, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> HashMap { + let resp = self.update_individual_files(algorithm, hashes, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + // TODO: Not all fields are tested currently in the v3 tests, only the v2-v3 relevant ones are + #[allow(clippy::too_many_arguments)] + pub async fn get_project_versions( + &self, + project_id_slug: &str, + game_versions: Option>, + loaders: Option>, + featured: Option, + version_type: Option, + limit: Option, + offset: Option, + pat: &str, + ) -> ServiceResponse { + let mut query_string = String::new(); + if let Some(game_versions) = game_versions { + query_string.push_str(&format!( + "&game_versions={}", + urlencoding::encode(&serde_json::to_string(&game_versions).unwrap()) + )); + } + if let Some(loaders) = loaders { + query_string.push_str(&format!( + "&loaders={}", + urlencoding::encode(&serde_json::to_string(&loaders).unwrap()) + )); + } + if let Some(featured) = featured { + query_string.push_str(&format!("&featured={}", featured)); + } + if let Some(version_type) = version_type { + query_string.push_str(&format!("&version_type={}", version_type)); + } + if let Some(limit) = limit { + let limit = limit.to_string(); + query_string.push_str(&format!("&limit={}", limit)); + } + if let Some(offset) = offset { + let offset = offset.to_string(); + query_string.push_str(&format!("&offset={}", offset)); + } + + let req = test::TestRequest::get() + .uri(&format!( + "/v3/project/{project_id_slug}/version?{}", + query_string.trim_start_matches('&') + )) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + #[allow(clippy::too_many_arguments)] + pub async fn get_project_versions_deserialized( + &self, + slug: &str, + game_versions: Option>, + loaders: Option>, + featured: Option, + version_type: Option, + limit: Option, + offset: Option, + pat: &str, + ) -> Vec { + let resp = self + .get_project_versions( + slug, + game_versions, + loaders, + featured, + version_type, + limit, + offset, + pat, + ) + .await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + // TODO: remove redundancy in these functions + + pub async fn create_default_version( + &self, + project_id: &str, + ordering: Option, + pat: &str, + ) -> Version { + let json_data = json!( + { + "project_id": project_id, + "file_parts": ["basic-mod-different.jar"], + "version_number": "1.2.3.4", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true, + "ordering": ordering, + } + ); + let json_segment = labrinth::util::actix::MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: labrinth::util::actix::MultipartSegmentData::Text( + serde_json::to_string(&json_data).unwrap(), + ), + }; + let file_segment = labrinth::util::actix::MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: labrinth::util::actix::MultipartSegmentData::Binary( + include_bytes!("../../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + let request = test::TestRequest::post() + .uri("/v3/version") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .append_header((AUTHORIZATION, pat)) + .to_request(); + let resp = self.call(request).await; + assert_status(&resp, StatusCode::OK); + test::read_body_json(resp).await + } + + pub async fn get_versions(&self, version_ids: Vec, pat: &str) -> Vec { + let ids = url_encode_json_serialized_vec(&version_ids); + let request = test::TestRequest::get() + .uri(&format!("/v3/versions?ids={}", ids)) + .append_header((AUTHORIZATION, pat)) + .to_request(); + let resp = self.call(request).await; + assert_status(&resp, StatusCode::OK); + test::read_body_json(resp).await + } + + pub async fn edit_version_ordering( + &self, + version_id: &str, + ordering: Option, + pat: &str, + ) -> ServiceResponse { + let request = test::TestRequest::patch() + .uri(&format!("/v3/version/{version_id}")) + .set_json(json!( + { + "ordering": ordering + } + )) + .append_header((AUTHORIZATION, pat)) + .to_request(); + self.call(request).await + } +} diff --git a/tests/common/asserts.rs b/tests/common/asserts.rs index 0c0d5464..cc6e35a6 100644 --- a/tests/common/asserts.rs +++ b/tests/common/asserts.rs @@ -2,13 +2,13 @@ use crate::common::get_json_val_str; use itertools::Itertools; -use labrinth::models::v2::projects::LegacyVersion; +use labrinth::models::v3::projects::Version; pub fn assert_status(response: &actix_web::dev::ServiceResponse, status: actix_http::StatusCode) { assert_eq!(response.status(), status, "{:#?}", response.response()); } -pub fn assert_version_ids(versions: &[LegacyVersion], expected_ids: Vec) { +pub fn assert_version_ids(versions: &[Version], expected_ids: Vec) { let version_ids = versions .iter() .map(|v| get_json_val_str(v.id)) diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index 2fe9a4df..aef26a71 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -7,7 +7,7 @@ use labrinth::models::{ oauth_clients::OAuthClient, organizations::Organization, pats::Scopes, - v2::projects::{LegacyProject, LegacyVersion}, + v3::projects::{Project, Version}, }; use serde_json::json; use sqlx::Executor; @@ -16,7 +16,7 @@ use zip::{write::FileOptions, CompressionMethod, ZipWriter}; use crate::common::database::USER_USER_PAT; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; -use super::{environment::TestEnvironment, request_data::get_public_project_creation_data}; +use super::{api_v3::request_data::get_public_project_creation_data, environment::TestEnvironment}; use super::{asserts::assert_status, database::USER_USER_ID, get_json_val_str}; @@ -170,10 +170,10 @@ pub struct DummyData { impl DummyData { pub fn new( - project_alpha: LegacyProject, - project_alpha_version: LegacyVersion, - project_beta: LegacyProject, - project_beta_version: LegacyVersion, + project_alpha: Project, + project_alpha_version: Version, + project_beta: Project, + project_beta_version: Version, organization_zeta: Organization, oauth_client_alpha: OAuthClient, ) -> Self { @@ -303,9 +303,9 @@ pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData { ) } -pub async fn add_project_alpha(test_env: &TestEnvironment) -> (LegacyProject, LegacyVersion) { +pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { let (project, versions) = test_env - .v2 + .v3 .add_public_project( get_public_project_creation_data("alpha", Some(TestFile::DummyProjectAlpha)), USER_USER_PAT, @@ -314,25 +314,25 @@ pub async fn add_project_alpha(test_env: &TestEnvironment) -> (LegacyProject, Le (project, versions.into_iter().next().unwrap()) } -pub async fn add_project_beta(test_env: &TestEnvironment) -> (LegacyProject, LegacyVersion) { +pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) { // Adds dummy data to the database with sqlx (projects, versions, threads) // Generate test project data. let jar = TestFile::DummyProjectBeta; + // TODO: this shouldnt be hardcoded (nor should other similar ones be) let json_data = json!( { "title": "Test Project Beta", "slug": "beta", "description": "A dummy project for testing with.", "body": "This project is not-yet-approved, and versions are draft.", - "client_side": "required", - "server_side": "optional", "initial_versions": [{ "file_parts": [jar.filename()], "version_number": "1.2.3", "version_title": "start", "status": "unlisted", - "requested_status": "unlisted", "dependencies": [], + "client_side": "required", + "server_side": "optional", "game_versions": ["1.20.1"] , "release_channel": "release", "loaders": ["fabric"], @@ -363,12 +363,11 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (LegacyProject, Leg // Add a project. let req = TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); get_project_beta(test_env).await @@ -377,7 +376,7 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (LegacyProject, Leg pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization { // Add an organzation. let req = TestRequest::post() - .uri("/v2/organization") + .uri("/v3/organization") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "title": "zeta", @@ -391,46 +390,46 @@ pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization { get_organization_zeta(test_env).await } -pub async fn get_project_alpha(test_env: &TestEnvironment) -> (LegacyProject, LegacyVersion) { +pub async fn get_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { // Get project let req = TestRequest::get() - .uri("/v2/project/alpha") + .uri("/v3/project/alpha") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; - let project: LegacyProject = test::read_body_json(resp).await; + let project: Project = test::read_body_json(resp).await; // Get project's versions let req = TestRequest::get() - .uri("/v2/project/alpha/version") + .uri("/v3/project/alpha/version") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; - let versions: Vec = test::read_body_json(resp).await; + let versions: Vec = test::read_body_json(resp).await; let version = versions.into_iter().next().unwrap(); (project, version) } -pub async fn get_project_beta(test_env: &TestEnvironment) -> (LegacyProject, LegacyVersion) { +pub async fn get_project_beta(test_env: &TestEnvironment) -> (Project, Version) { // Get project let req = TestRequest::get() - .uri("/v2/project/beta") + .uri("/v3/project/beta") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_status(&resp, StatusCode::OK); let project: serde_json::Value = test::read_body_json(resp).await; - let project: LegacyProject = serde_json::from_value(project).unwrap(); + let project: Project = serde_json::from_value(project).unwrap(); // Get project's versions let req = TestRequest::get() - .uri("/v2/project/beta/version") + .uri("/v3/project/beta/version") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_status(&resp, StatusCode::OK); - let versions: Vec = test::read_body_json(resp).await; + let versions: Vec = test::read_body_json(resp).await; let version = versions.into_iter().next().unwrap(); (project, version) @@ -439,7 +438,7 @@ pub async fn get_project_beta(test_env: &TestEnvironment) -> (LegacyProject, Leg pub async fn get_organization_zeta(test_env: &TestEnvironment) -> Organization { // Get organization let req = TestRequest::get() - .uri("/v2/organization/zeta") + .uri("/v3/organization/zeta") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; diff --git a/tests/common/environment.rs b/tests/common/environment.rs index 55fd82fa..9f91a509 100644 --- a/tests/common/environment.rs +++ b/tests/common/environment.rs @@ -25,6 +25,9 @@ where db.cleanup().await; } +// TODO: This needs to be slightly redesigned in order to do both V2 and v3 tests. +// TODO: Most tests, since they use API functions, can be applied to both. The ones that weren't are in v2/, but +// all tests that can be applied to both should use both v2 and v3 (extract api to a trait with all the API functions and call both). // A complete test environment, with a test actix app and a database. // Must be called in an #[actix_rt::test] context. It also simulates a @@ -77,7 +80,7 @@ impl TestEnvironment { pub async fn generate_friend_user_notification(&self) { let resp = self - .v2 + .v3 .add_user_to_team( &self.dummy.as_ref().unwrap().project_alpha.team_id, FRIEND_USER_ID, @@ -95,7 +98,7 @@ impl TestEnvironment { pat: &str, status_code: StatusCode, ) { - let resp = self.v2.get_user_notifications(user_id, pat).await; + let resp = self.v3.get_user_notifications(user_id, pat).await; assert_status(&resp, status_code); } @@ -105,7 +108,7 @@ impl TestEnvironment { pat: &str, status_code: StatusCode, ) { - let resp = self.v2.get_user_projects(user_id, pat).await; + let resp = self.v3.get_user_projects(user_id, pat).await; assert_status(&resp, status_code); } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 84ac23ab..f26a0c73 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,7 +10,6 @@ pub mod dummy_data; pub mod environment; pub mod pats; pub mod permissions; -pub mod request_data; pub mod scopes; // Testing equivalent to 'setup' function, producing a LabrinthConfig diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs index 22dbdd8a..1bb2e20a 100644 --- a/tests/common/permissions.rs +++ b/tests/common/permissions.rs @@ -4,13 +4,11 @@ use itertools::Itertools; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; -use crate::common::{ - database::{generate_random_name, ADMIN_USER_PAT}, - request_data, -}; +use crate::common::database::{generate_random_name, ADMIN_USER_PAT}; use super::{ - database::{USER_USER_ID, USER_USER_PAT}, + api_v3::request_data, + database::{ENEMY_USER_PAT, USER_USER_ID, USER_USER_PAT}, environment::TestEnvironment, }; @@ -164,7 +162,48 @@ impl<'a> PermissionsTest<'a> { ) .await; - // Failure test + // Failure test- not logged in + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Failure permissions test failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + // Failure test- logged in on a non-team user + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", ENEMY_USER_PAT)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Failure permissions test failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + // Failure test- logged in with EVERY non-relevant permission let request = req_gen(&PermissionsTestContext { project_id: Some(&project_id), team_id: Some(&team_id), @@ -175,7 +214,6 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { - println!("Body: {:?}", resp.response().body()); return Err(format!( "Failure permissions test failed. Expected failure codes {} got {}", self.allowed_failure_codes @@ -207,7 +245,6 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !resp.status().is_success() { - println!("Body: {:?}", resp.response().body()); return Err(format!( "Success permissions test failed. Expected success, got {}", resp.status().as_u16() @@ -342,8 +379,9 @@ impl<'a> PermissionsTest<'a> { organization_team_id: None, }; - // TEST 1: Failure - // Random user, unaffiliated with the project, with no permissions + // TEST 1: User not logged in - no PAT. + // This should always fail, regardless of permissions + // (As we are testing permissions-based failures) let test_1 = async { let (project_id, team_id) = create_dummy_project(test_env).await; @@ -379,9 +417,45 @@ impl<'a> PermissionsTest<'a> { }; // TEST 2: Failure - // User affiliated with the project, with failure permissions + // Random user, unaffiliated with the project, with no permissions let test_2 = async { let (project_id, team_id) = create_dummy_project(test_env).await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 2 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != ProjectPermissions::empty() { + return Err(format!( + "Test 2 failed. Expected no permissions, got {:?}", + p + )); + } + + Ok(()) + }; + + // TEST 3: Failure + // User affiliated with the project, with failure permissions + let test_3 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; add_user_to_team( self.user_id, self.user_pat, @@ -403,7 +477,7 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( - "Test 2 failed. Expected failure codes {} got {}", + "Test 3 failed. Expected failure codes {} got {}", self.allowed_failure_codes .iter() .map(|code| code.to_string()) @@ -416,7 +490,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != failure_project_permissions { return Err(format!( - "Test 2 failed. Expected {:?}, got {:?}", + "Test 3 failed. Expected {:?}, got {:?}", failure_project_permissions, p )); } @@ -424,9 +498,9 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - // TEST 3: Success + // TEST 4: Success // User affiliated with the project, with the given permissions - let test_3 = async { + let test_4 = async { let (project_id, team_id) = create_dummy_project(test_env).await; add_user_to_team( self.user_id, @@ -449,7 +523,7 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !resp.status().is_success() { return Err(format!( - "Test 3 failed. Expected success, got {}", + "Test 4 failed. Expected success, got {}", resp.status().as_u16() )); } @@ -458,7 +532,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != success_permissions { return Err(format!( - "Test 3 failed. Expected {:?}, got {:?}", + "Test 4 failed. Expected {:?}, got {:?}", success_permissions, p )); } @@ -466,10 +540,10 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - // TEST 4: Failure + // TEST 5: Failure // Project has an organization // User affiliated with the project's org, with default failure permissions - let test_4 = async { + let test_5 = async { let (project_id, team_id) = create_dummy_project(test_env).await; let (organization_id, organization_team_id) = create_dummy_org(test_env).await; add_project_to_org(test_env, &project_id, &organization_id).await; @@ -494,7 +568,7 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( - "Test 4 failed. Expected failure codes {} got {}", + "Test 5 failed. Expected failure codes {} got {}", self.allowed_failure_codes .iter() .map(|code| code.to_string()) @@ -507,7 +581,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != failure_project_permissions { return Err(format!( - "Test 4 failed. Expected {:?}, got {:?}", + "Test 5 failed. Expected {:?}, got {:?}", failure_project_permissions, p )); } @@ -515,10 +589,10 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - // TEST 5: Success + // TEST 6: Success // Project has an organization // User affiliated with the project's org, with the default success - let test_5 = async { + let test_6 = async { let (project_id, team_id) = create_dummy_project(test_env).await; let (organization_id, organization_team_id) = create_dummy_org(test_env).await; add_project_to_org(test_env, &project_id, &organization_id).await; @@ -543,7 +617,7 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !resp.status().is_success() { return Err(format!( - "Test 5 failed. Expected success, got {}", + "Test 6 failed. Expected success, got {}", resp.status().as_u16() )); } @@ -552,7 +626,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != success_permissions { return Err(format!( - "Test 5 failed. Expected {:?}, got {:?}", + "Test 6 failed. Expected {:?}, got {:?}", success_permissions, p )); } @@ -560,11 +634,11 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - // TEST 6: Failure + // TEST 7: Failure // Project has an organization // User affiliated with the project's org (even can have successful permissions!) // User overwritten on the project team with failure permissions - let test_6 = async { + let test_7 = async { let (project_id, team_id) = create_dummy_project(test_env).await; let (organization_id, organization_team_id) = create_dummy_org(test_env).await; add_project_to_org(test_env, &project_id, &organization_id).await; @@ -598,7 +672,7 @@ impl<'a> PermissionsTest<'a> { let resp = test_env.call(request).await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( - "Test 6 failed. Expected failure codes {} got {}", + "Test 7 failed. Expected failure codes {} got {}", self.allowed_failure_codes .iter() .map(|code| code.to_string()) @@ -611,7 +685,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != failure_project_permissions { return Err(format!( - "Test 6 failed. Expected {:?}, got {:?}", + "Test 7 failed. Expected {:?}, got {:?}", failure_project_permissions, p )); } @@ -619,11 +693,11 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - // TEST 7: Success + // TEST 8: Success // Project has an organization // User affiliated with the project's org with default failure permissions // User overwritten to the project with the success permissions - let test_7 = async { + let test_8 = async { let (project_id, team_id) = create_dummy_project(test_env).await; let (organization_id, organization_team_id) = create_dummy_org(test_env).await; add_project_to_org(test_env, &project_id, &organization_id).await; @@ -658,7 +732,7 @@ impl<'a> PermissionsTest<'a> { if !resp.status().is_success() { return Err(format!( - "Test 7 failed. Expected success, got {}", + "Test 8 failed. Expected success, got {}", resp.status().as_u16() )); } @@ -667,7 +741,7 @@ impl<'a> PermissionsTest<'a> { get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; if p != success_permissions { return Err(format!( - "Test 7 failed. Expected {:?}, got {:?}", + "Test 8 failed. Expected {:?}, got {:?}", success_permissions, p )); } @@ -675,7 +749,8 @@ impl<'a> PermissionsTest<'a> { Ok(()) }; - tokio::try_join!(test_1, test_2, test_3, test_4, test_5, test_6, test_7,).map_err(|e| e)?; + tokio::try_join!(test_1, test_2, test_3, test_4, test_5, test_6, test_7, test_8) + .map_err(|e| e)?; Ok(()) } @@ -845,7 +920,7 @@ impl<'a> PermissionsTest<'a> { } async fn create_dummy_project(test_env: &TestEnvironment) -> (String, String) { - let api = &test_env.v2; + let api = &test_env.v3; // Create a very simple project let slug = generate_random_name("test_project"); @@ -861,7 +936,7 @@ async fn create_dummy_project(test_env: &TestEnvironment) -> (String, String) { async fn create_dummy_org(test_env: &TestEnvironment) -> (String, String) { // Create a very simple organization let name = generate_random_name("test_org"); - let api = &test_env.v2; + let api = &test_env.v3; let resp = api .create_organization(&name, "Example description.", ADMIN_USER_PAT) @@ -878,7 +953,7 @@ async fn create_dummy_org(test_env: &TestEnvironment) -> (String, String) { } async fn add_project_to_org(test_env: &TestEnvironment, project_id: &str, organization_id: &str) { - let api = &test_env.v2; + let api = &test_env.v3; let resp = api .organization_add_project(organization_id, project_id, ADMIN_USER_PAT) .await; @@ -893,7 +968,7 @@ async fn add_user_to_team( organization_permissions: Option, test_env: &TestEnvironment, ) { - let api = &test_env.v2; + let api = &test_env.v3; // Invite user let resp = api @@ -919,7 +994,7 @@ async fn modify_user_team_permissions( organization_permissions: Option, test_env: &TestEnvironment, ) { - let api = &test_env.v2; + let api = &test_env.v3; // Send invitation to user let resp = api @@ -938,7 +1013,7 @@ async fn modify_user_team_permissions( async fn remove_user_from_team(user_id: &str, team_id: &str, test_env: &TestEnvironment) { // Send invitation to user - let api = &test_env.v2; + let api = &test_env.v3; let resp = api.remove_from_team(team_id, user_id, ADMIN_USER_PAT).await; assert!(resp.status().is_success()); } @@ -949,7 +1024,7 @@ async fn get_project_permissions( project_id: &str, test_env: &TestEnvironment, ) -> ProjectPermissions { - let resp = test_env.v2.get_project_members(project_id, user_pat).await; + let resp = test_env.v3.get_project_members(project_id, user_pat).await; let permissions = if resp.status().as_u16() == 200 { let value: serde_json::Value = test::read_body_json(resp).await; value @@ -972,7 +1047,7 @@ async fn get_organization_permissions( organization_id: &str, test_env: &TestEnvironment, ) -> OrganizationPermissions { - let api = &test_env.v2; + let api = &test_env.v3; let resp = api .get_organization_members(organization_id, user_pat) .await; diff --git a/tests/files/dummy_data.sql b/tests/files/dummy_data.sql index 58197c68..8f2e4707 100644 --- a/tests/files/dummy_data.sql +++ b/tests/files/dummy_data.sql @@ -32,6 +32,19 @@ INSERT INTO loader_field_enum_values (enum_id, value) SELECT id, 'forge' FROM lo INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) SELECT joining_loader_id, joining_project_type_id, 1 FROM loaders_project_types WHERE joining_loader_id = 5; INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) SELECT joining_loader_id, joining_project_type_id, 1 FROM loaders_project_types WHERE joining_loader_id = 6; +-- Dummy-data only optional field, as we don't have any yet +INSERT INTO loader_fields ( + field, + field_type, + optional +) VALUES ( + 'test_fabric_optional', + 'integer', + true +); +INSERT INTO loader_fields_loaders(loader_id, loader_field_id) +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'test_fabric_optional' AND l.loader = 'fabric'; + -- Sample game versions, loaders, categories -- Game versions is '2' INSERT INTO loader_field_enum_values(enum_id, value, metadata, created) diff --git a/tests/loader_fields.rs b/tests/loader_fields.rs new file mode 100644 index 00000000..b7f65e34 --- /dev/null +++ b/tests/loader_fields.rs @@ -0,0 +1,306 @@ +use std::collections::HashSet; + +use common::environment::TestEnvironment; +use serde_json::json; + +use crate::common::api_v3::request_data::get_public_version_creation_data; +use crate::common::database::*; + +use crate::common::dummy_data::TestFile; + +// importing common module. +mod common; + +#[actix_rt::test] +async fn creating_loader_fields() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v3; + + let alpha_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + let alpha_project_id = serde_json::from_str(&format!("\"{}\"", alpha_project_id)).unwrap(); + let alpha_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); + + // ALL THE FOLLOWING FOR CREATE AND PATCH + // Cannot create a version with an extra argument that cannot be tied to a loader field ("invalid loader field") + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + j["invalid"] = json!("invalid"); + }), + ); + let resp = api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "invalid": "invalid" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + + // Cannot create a version with a loader field that isnt used by the loader + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + // This is only for mrpacks, not mods/jars + j["mrpack_loaders"] = json!(["fabric"]); + }), + ); + let resp = api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "mrpack_loaders": ["fabric"] + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + + // Cannot create a version without an applicable loader field that is not optional + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + let j = j.as_object_mut().unwrap(); + j.remove("client_side"); + }), + ); + let resp = api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // Cannot create a version without a loader field array that has a minimum of 1 + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + let j = j.as_object_mut().unwrap(); + j.remove("game_versions"); + }), + ); + let resp = api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // TODO: Create a test for too many elements in the array when we have a LF that has a max (past max) + // Cannot create a version with a loader field array that has fewer than the minimum elements + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + let j: &mut serde_json::Map = j.as_object_mut().unwrap(); + j["game_versions"] = json!([]); + }), + ); + let resp: actix_web::dev::ServiceResponse = + api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": [] + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + + // Cannot create an invalid data type for the loader field type (including bad variant for the type) + for bad_type_game_versions in [ + json!(1), + json!([1]), + json!("1.20.1"), + json!(["client_side"]), + ] { + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + let j: &mut serde_json::Map = j.as_object_mut().unwrap(); + j["game_versions"] = bad_type_game_versions.clone(); + }), + ); + let resp = api.add_public_version(version_data, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": bad_type_game_versions + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Can create with optional loader fields (other tests have checked if we can create without them) + // TODO: - Create project + // - Create version + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + j["test_fabric_optional"] = json!(555); + }), + ); + let v = api + .add_public_version_deserialized(version_data, USER_USER_PAT) + .await; + assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "test_fabric_optional": 555 + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + let v = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); + + // Simply setting them as expected works + // - Create + let version_data = get_public_version_creation_data( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + Some(|j: &mut serde_json::Value| { + let j: &mut serde_json::Map = j.as_object_mut().unwrap(); + j["game_versions"] = json!(["1.20.1", "1.20.2"]); + j["client_side"] = json!("optional"); + j["server_side"] = json!("required"); + }), + ); + let v = api + .add_public_version_deserialized(version_data, USER_USER_PAT) + .await; + assert_eq!( + v.fields.get("game_versions").unwrap(), + &json!(["1.20.1", "1.20.2"]) + ); + assert_eq!(v.fields.get("client_side").unwrap(), &json!("optional")); + assert_eq!(v.fields.get("server_side").unwrap(), &json!("required")); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": ["1.20.1", "1.20.2"], + "client_side": "optional", + "server_side": "required" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + let v = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!( + v.fields.get("game_versions").unwrap(), + &json!(["1.20.1", "1.20.2"]) + ); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn get_loader_fields() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v3; + + let game_versions = api + .get_loader_field_variants_deserialized("game_versions") + .await; + let side_types = api + .get_loader_field_variants_deserialized("client_side") + .await; + + // These tests match dummy data and will need to be updated if the dummy data changes + // Versions should be ordered by: + // - ordering + // - ordering ties settled by date added to database + // - We also expect presentation of NEWEST to OLDEST + // - All null orderings are treated as older than any non-null ordering + // (for this test, the 1.20.1, etc, versions are all null ordering) + let game_version_versions = game_versions + .into_iter() + .map(|x| x.value) + .collect::>(); + assert_eq!( + game_version_versions, + [ + "Ordering_Negative1", + "Ordering_Positive100", + "1.20.5", + "1.20.4", + "1.20.3", + "1.20.2", + "1.20.1" + ] + ); + + let side_type_names = side_types + .into_iter() + .map(|x| x.value) + .collect::>(); + assert_eq!( + side_type_names, + ["unknown", "required", "optional", "unsupported"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + test_env.cleanup().await; +} diff --git a/tests/notifications.rs b/tests/notifications.rs index 29dd4b7d..52a71755 100644 --- a/tests/notifications.rs +++ b/tests/notifications.rs @@ -15,7 +15,7 @@ pub async fn get_user_notifications_after_team_invitation_returns_notification() .project_alpha .team_id .clone(); - let api = test_env.v2; + let api = test_env.v3; api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; @@ -34,7 +34,7 @@ pub async fn get_user_notifications_after_team_invitation_returns_notification() pub async fn get_user_notifications_after_reading_indicates_notification_read() { with_test_environment(|test_env| async move { test_env.generate_friend_user_notification().await; - let api = test_env.v2; + let api = test_env.v3; let notifications = api .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; @@ -57,7 +57,7 @@ pub async fn get_user_notifications_after_reading_indicates_notification_read() pub async fn get_user_notifications_after_deleting_does_not_show_notification() { with_test_environment(|test_env| async move { test_env.generate_friend_user_notification().await; - let api = test_env.v2; + let api = test_env.v3; let notifications = api .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; diff --git a/tests/oauth.rs b/tests/oauth.rs index 63deb036..76e7ad85 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -1,13 +1,12 @@ -use crate::common::{ - api_v3::oauth::get_redirect_location_query_params, database::FRIEND_USER_ID, - dummy_data::DummyOAuthClientAlpha, -}; use actix_http::StatusCode; -use actix_web::test::{self}; +use actix_web::test; use common::{ + api_v3::oauth::get_redirect_location_query_params, api_v3::oauth::{get_auth_code_from_redirect_params, get_authorize_accept_flow_id}, asserts::{assert_any_status_except, assert_status}, + database::FRIEND_USER_ID, database::{FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, + dummy_data::DummyOAuthClientAlpha, environment::with_test_environment, }; use labrinth::auth::oauth::TokenResponse; diff --git a/tests/oauth_clients.rs b/tests/oauth_clients.rs index 257bbc90..d4eef9f1 100644 --- a/tests/oauth_clients.rs +++ b/tests/oauth_clients.rs @@ -14,7 +14,7 @@ use labrinth::{ routes::v3::oauth_clients::OAuthClientEdit, }; -use crate::common::{asserts::assert_status, database::USER_USER_ID_PARSED}; +use common::{asserts::assert_status, database::USER_USER_ID_PARSED}; mod common; diff --git a/tests/organizations.rs b/tests/organizations.rs index edbb84bd..66ef7def 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -1,8 +1,8 @@ use crate::common::{ + api_v3::request_data::get_icon_data, database::{generate_random_name, ADMIN_USER_PAT, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID}, dummy_data::DummyImage, environment::TestEnvironment, - request_data::get_icon_data, }; use actix_web::test; use bytes::Bytes; @@ -18,7 +18,7 @@ mod common; #[actix_rt::test] async fn create_organization() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let zeta_organization_slug = &test_env .dummy .as_ref() @@ -86,7 +86,7 @@ async fn create_organization() { #[actix_rt::test] async fn patch_organization() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let zeta_organization_id = &test_env .dummy @@ -168,7 +168,7 @@ async fn patch_organization() { #[actix_rt::test] async fn add_remove_icon() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let zeta_organization_id = &test_env .dummy .as_ref() @@ -178,7 +178,7 @@ async fn add_remove_icon() { // Get project let resp = test_env - .v2 + .v3 .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) .await; assert_eq!(resp.icon_url, None); @@ -220,7 +220,7 @@ async fn add_remove_icon() { #[actix_rt::test] async fn delete_org() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let zeta_organization_id = &test_env .dummy .as_ref() @@ -258,14 +258,14 @@ async fn add_remove_organization_projects() { // Add/remove project to organization, first by ID, then by slug for alpha in [alpha_project_id, alpha_project_slug] { let resp = test_env - .v2 + .v3 .organization_add_project(zeta_organization_id, alpha, USER_USER_PAT) .await; assert_eq!(resp.status(), 200); // Get organization projects let projects = test_env - .v2 + .v3 .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) .await; assert_eq!(projects[0].id.to_string(), alpha_project_id); @@ -273,14 +273,14 @@ async fn add_remove_organization_projects() { // Remove project from organization let resp = test_env - .v2 + .v3 .organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT) .await; assert_eq!(resp.status(), 200); // Get organization projects let projects = test_env - .v2 + .v3 .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) .await; assert!(projects.is_empty()); @@ -304,7 +304,7 @@ async fn permissions_patch_organization() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/organization/{}", + "/v3/organization/{}", ctx.organization_id.unwrap() )) .set_json(json!({ @@ -344,7 +344,7 @@ async fn permissions_edit_details() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/organization/{}/icon?ext=png", + "/v3/organization/{}/icon?ext=png", ctx.organization_id.unwrap() )) .set_payload(Bytes::from( @@ -362,7 +362,7 @@ async fn permissions_edit_details() { // Uses alpha project to delete added icon let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/organization/{}/icon?ext=png", + "/v3/organization/{}/icon?ext=png", ctx.organization_id.unwrap() )) }; @@ -378,7 +378,7 @@ async fn permissions_edit_details() { async fn permissions_manage_invites() { // Add member, remove member, edit member let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let zeta_organization_id = &test_env .dummy @@ -393,7 +393,7 @@ async fn permissions_manage_invites() { // Add member let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() - .uri(&format!("/v2/team/{}/members", ctx.team_id.unwrap())) + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) .set_json(json!({ "user_id": MOD_USER_ID, "permissions": 0, @@ -412,7 +412,7 @@ async fn permissions_manage_invites() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) .set_json(json!({ @@ -430,7 +430,7 @@ async fn permissions_manage_invites() { // requires manage_invites if they have not yet accepted the invite let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; @@ -453,7 +453,7 @@ async fn permissions_manage_invites() { let remove_member = OrganizationPermissions::REMOVE_MEMBER; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; @@ -471,7 +471,7 @@ async fn permissions_manage_invites() { #[actix_rt::test] async fn permissions_add_remove_project() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; @@ -503,7 +503,7 @@ async fn permissions_add_remove_project() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() .uri(&format!( - "/v2/organization/{}/projects", + "/v3/organization/{}/projects", ctx.organization_id.unwrap() )) .set_json(json!({ @@ -521,7 +521,7 @@ async fn permissions_add_remove_project() { let remove_project = OrganizationPermissions::REMOVE_PROJECT; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/organization/{}/projects/{alpha_project_id}", + "/v3/organization/{}/projects/{alpha_project_id}", ctx.organization_id.unwrap() )) }; @@ -544,7 +544,7 @@ async fn permissions_delete_organization() { // Add alpha project to zeta organization let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/organization/{}", + "/v3/organization/{}", ctx.organization_id.unwrap() )) }; @@ -578,10 +578,10 @@ async fn permissions_add_default_project_permissions() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() - .uri(&format!("/v2/team/{}/members", ctx.team_id.unwrap())) + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) .set_json(json!({ "user_id": MOD_USER_ID, - // do not set permissions as it will be set to default, which is non-zero + "permissions": (ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION).bits(), "organization_permissions": 0, })) }; @@ -606,7 +606,7 @@ async fn permissions_add_default_project_permissions() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) .set_json(json!({ @@ -633,7 +633,7 @@ async fn permissions_organization_permissions_consistency_test() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/organization/{}", + "/v3/organization/{}", ctx.organization_id.unwrap() )) .set_json(json!({ diff --git a/tests/pats.rs b/tests/pats.rs index fe60639e..a46b4e63 100644 --- a/tests/pats.rs +++ b/tests/pats.rs @@ -1,12 +1,10 @@ use actix_web::test; use chrono::{Duration, Utc}; use common::database::*; +use common::environment::TestEnvironment; use labrinth::models::pats::Scopes; use serde_json::json; -use crate::common::environment::TestEnvironment; - -// importing common module. mod common; // Full pat test: @@ -22,7 +20,7 @@ pub async fn pat_full_test() { // Create a PAT for a full test let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -46,7 +44,7 @@ pub async fn pat_full_test() { // Get PAT again let req = test::TestRequest::get() .append_header(("Authorization", USER_USER_PAT)) - .uri("/v2/pat") + .uri("/v3/pat") .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status().as_u16(), 200); @@ -62,7 +60,7 @@ pub async fn pat_full_test() { let token = token.to_string(); async { let req = test::TestRequest::post() - .uri("/v2/collection") + .uri("/v3/collection") .append_header(("Authorization", token)) .set_json(json!({ "title": "Test Collection 1", @@ -78,7 +76,7 @@ pub async fn pat_full_test() { // Change scopes and test again let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": 0, @@ -90,7 +88,7 @@ pub async fn pat_full_test() { // Change scopes back, and set expiry to the past, and test again let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, @@ -106,7 +104,7 @@ pub async fn pat_full_test() { // Change everything back to normal and test again let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() + Duration::days(1), // no longer expired! @@ -118,7 +116,7 @@ pub async fn pat_full_test() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past @@ -135,7 +133,7 @@ pub async fn pat_full_test() { } let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), @@ -151,7 +149,7 @@ pub async fn pat_full_test() { // Delete PAT let req = test::TestRequest::delete() .append_header(("Authorization", USER_USER_PAT)) - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status().as_u16(), 204); @@ -167,7 +165,7 @@ pub async fn bad_pats() { // Creating a PAT with no name should fail let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -180,7 +178,7 @@ pub async fn bad_pats() { // Name too short or too long should fail for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "name": name, @@ -194,7 +192,7 @@ pub async fn bad_pats() { // Creating a PAT with an expiry in the past should fail let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -212,7 +210,7 @@ pub async fn bad_pats() { continue; } let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), @@ -229,7 +227,7 @@ pub async fn bad_pats() { // Create a 'good' PAT for patching let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, @@ -245,7 +243,7 @@ pub async fn bad_pats() { // Patching to a bad name should fail for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() - .uri("/v2/pat") + .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "name": name, @@ -257,7 +255,7 @@ pub async fn bad_pats() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past @@ -274,7 +272,7 @@ pub async fn bad_pats() { } let req = test::TestRequest::patch() - .uri(&format!("/v2/pat/{}", id)) + .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), diff --git a/tests/project.rs b/tests/project.rs index 5bdf5890..138acac5 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -2,21 +2,17 @@ use actix_http::StatusCode; use actix_web::test; use bytes::Bytes; use chrono::{Duration, Utc}; +use common::database::*; +use common::dummy_data::DUMMY_CATEGORIES; use common::environment::{with_test_environment, TestEnvironment}; use common::permissions::{PermissionsTest, PermissionsTestContext}; use futures::StreamExt; -use itertools::Itertools; use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; -use crate::common::{database::*, request_data}; - -use crate::common::dummy_data::{TestFile, DUMMY_CATEGORIES}; - -// importing common module. mod common; #[actix_rt::test] @@ -30,7 +26,7 @@ async fn test_get_project() { // Perform request on dummy data let req = test::TestRequest::get() - .uri(&format!("/v2/project/{alpha_project_id}")) + .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -66,7 +62,7 @@ async fn test_get_project() { // Make the request again, this time it should be cached let req = test::TestRequest::get() - .uri(&format!("/v2/project/{alpha_project_id}")) + .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -79,7 +75,7 @@ async fn test_get_project() { // Request should fail on non-existent project let req = test::TestRequest::get() - .uri("/v2/project/nonexistent") + .uri("/v3/project/nonexistent") .append_header(("Authorization", USER_USER_PAT)) .to_request(); @@ -88,7 +84,7 @@ async fn test_get_project() { // Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project) let req = test::TestRequest::get() - .uri(&format!("/v2/project/{beta_project_id}")) + .uri(&format!("/v3/project/{beta_project_id}")) .append_header(("Authorization", ENEMY_USER_PAT)) .to_request(); @@ -103,7 +99,7 @@ async fn test_get_project() { async fn test_add_remove_project() { // Test setup and dummy data let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; // Generate test project data. let mut json_data = json!( @@ -112,14 +108,14 @@ async fn test_add_remove_project() { "slug": "demo", "description": "Example description.", "body": "Example body.", - "client_side": "required", - "server_side": "optional", "initial_versions": [{ "file_parts": ["basic-mod.jar"], "version_number": "1.2.3", "version_title": "start", "dependencies": [], "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", "release_channel": "release", "loaders": ["fabric"], "featured": true @@ -157,6 +153,7 @@ async fn test_add_remove_project() { name: "basic-mod.jar".to_string(), filename: Some("basic-mod.jar".to_string()), content_type: Some("application/java-archive".to_string()), + // TODO: look at these: can be used in the reuse data data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), }; @@ -180,7 +177,7 @@ async fn test_add_remove_project() { // Add a project- simple, should work. let req = test::TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); @@ -206,7 +203,7 @@ async fn test_add_remove_project() { // Reusing with a different slug and the same file should fail // Even if that file is named differently let req = test::TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_slug_file_segment.clone(), // Different slug, different file name @@ -219,7 +216,7 @@ async fn test_add_remove_project() { // Reusing with the same slug and a different file should fail let req = test::TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_file_segment.clone(), // Same slug, different file name @@ -232,7 +229,7 @@ async fn test_add_remove_project() { // Different slug, different file should succeed let req = test::TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_slug_file_segment.clone(), // Different slug, different file name @@ -248,7 +245,7 @@ async fn test_add_remove_project() { let id = project.id.to_string(); // Remove the project - let resp = test_env.v2.remove_project("demo", USER_USER_PAT).await; + let resp = test_env.v3.remove_project("demo", USER_USER_PAT).await; assert_eq!(resp.status(), 204); // Confirm that the project is gone from the cache @@ -279,59 +276,10 @@ async fn test_add_remove_project() { test_env.cleanup().await; } -#[actix_rt::test] -async fn test_project_type_sanity() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - // Perform all other patch tests on both 'mod' and 'modpack' - let test_creation_mod = request_data::get_public_project_creation_data( - "test-mod", - Some(TestFile::build_random_jar()), - ); - let test_creation_modpack = request_data::get_public_project_creation_data( - "test-modpack", - Some(TestFile::build_random_mrpack()), - ); - for (mod_or_modpack, test_creation_data) in [ - ("mod", test_creation_mod), - ("modpack", test_creation_modpack), - ] { - let (test_project, test_version) = api - .add_public_project(test_creation_data, USER_USER_PAT) - .await; - let test_project_slug = test_project.slug.as_ref().unwrap(); - - assert_eq!(test_project.project_type, mod_or_modpack); - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!( - test_version[0].loaders.iter().map(|x| &x.0).collect_vec(), - vec!["fabric"] - ); - - let project = api - .get_project_deserialized(test_project_slug, USER_USER_PAT) - .await; - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!(project.project_type, mod_or_modpack); - - let version = api - .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) - .await; - assert_eq!( - version.loaders.iter().map(|x| &x.0).collect_vec(), - vec!["fabric"] - ); - } - - // TODO: as we get more complicated strucures with v3 testing, and alpha/beta get more complicated, we should add more tests here, - // to ensure that projects created with v3 routes are still valid and work with v2 routes. -} - #[actix_rt::test] pub async fn test_patch_project() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; @@ -453,8 +401,6 @@ pub async fn test_patch_project() { "issues_url": "https://github.com", "discord_url": "https://discord.gg", "wiki_url": "https://wiki.com", - "client_side": "optional", - "server_side": "required", "donation_urls": [{ "id": "patreon", "platform": "Patreon", @@ -471,30 +417,21 @@ pub async fn test_patch_project() { assert_eq!(resp.status(), 404); // New slug does work - let resp = api.get_project("newslug", USER_USER_PAT).await; - let project: serde_json::Value = test::read_body_json(resp).await; - - assert_eq!(project["slug"], json!(Some("newslug".to_string()))); - assert_eq!(project["title"], "New successful title"); - assert_eq!(project["description"], "New successful description"); - assert_eq!(project["body"], "New successful body"); - assert_eq!(project["categories"], json!(vec![DUMMY_CATEGORIES[0]])); - assert_eq!(project["license"]["id"], "MIT"); - assert_eq!( - project["issues_url"], - json!(Some("https://github.com".to_string())) - ); - assert_eq!( - project["discord_url"], - json!(Some("https://discord.gg".to_string())) - ); - assert_eq!( - project["wiki_url"], - json!(Some("https://wiki.com".to_string())) - ); - assert_eq!(project["client_side"], json!("optional")); - assert_eq!(project["server_side"], json!("required")); - assert_eq!(project["donation_urls"][0]["url"], "https://patreon.com"); + let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; + + assert_eq!(project.slug.unwrap(), "newslug"); + assert_eq!(project.title, "New successful title"); + assert_eq!(project.description, "New successful description"); + assert_eq!(project.body, "New successful body"); + assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); + assert_eq!(project.license.id, "MIT"); + assert_eq!(project.issues_url, Some("https://github.com".to_string())); + assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); + assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); + assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); + + // TODO: + // for loader fields? // Cleanup test db test_env.cleanup().await; @@ -503,7 +440,7 @@ pub async fn test_patch_project() { #[actix_rt::test] pub async fn test_bulk_edit_categories() { with_test_environment(|test_env| async move { - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let beta_project_id: &str = &test_env.dummy.as_ref().unwrap().project_beta.project_id; @@ -555,8 +492,6 @@ async fn permissions_patch_project() { ("title", json!("randomname")), ("description", json!("randomdescription")), ("categories", json!(["combat", "economy"])), - // ("client_side", json!("unsupported")), - // ("server_side", json!("unsupported")), ("additional_categories", json!(["decoration"])), ("issues_url", json!("https://issues.com")), ("source_url", json!("https://source.com")), @@ -579,7 +514,7 @@ async fn permissions_patch_project() { async move { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ key: if key == "slug" { json!(generate_random_name("randomslug")) @@ -602,7 +537,7 @@ async fn permissions_patch_project() { // This requires a project with a version, so we use alpha_project_id let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "status": "private", "requested_status": "private", @@ -619,7 +554,7 @@ async fn permissions_patch_project() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/projects?ids=[{uri}]", + "/v3/projects?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{}\"", ctx.project_id.unwrap())) )) .set_json(json!({ @@ -636,7 +571,7 @@ async fn permissions_patch_project() { let edit_body = ProjectPermissions::EDIT_BODY; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "body": "new body!", })) @@ -663,7 +598,7 @@ async fn permissions_edit_details() { // Approve beta version as private so we can schedule it let req = test::TestRequest::patch() - .uri(&format!("/v2/version/{beta_version_id}")) + .uri(&format!("/v3/version/{beta_version_id}")) .append_header(("Authorization", MOD_USER_PAT)) .set_json(json!({ "status": "unlisted" @@ -675,7 +610,7 @@ async fn permissions_edit_details() { // Schedule version let req_gen = |_: &PermissionsTestContext| { test::TestRequest::post() - .uri(&format!("/v2/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it + .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it .set_json(json!( { "requested_status": "archived", @@ -695,7 +630,7 @@ async fn permissions_edit_details() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/project/{}/icon?ext=png", + "/v3/project/{}/icon?ext=png", ctx.project_id.unwrap() )) .set_payload(Bytes::from( @@ -713,7 +648,7 @@ async fn permissions_edit_details() { // Uses alpha project to delete added icon let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/project/{}/icon?ext=png", + "/v3/project/{}/icon?ext=png", ctx.project_id.unwrap() )) }; @@ -729,7 +664,7 @@ async fn permissions_edit_details() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() .uri(&format!( - "/v2/project/{}/gallery?ext=png&featured=true", + "/v3/project/{}/gallery?ext=png&featured=true", ctx.project_id.unwrap() )) .set_payload(Bytes::from( @@ -744,7 +679,7 @@ async fn permissions_edit_details() { .unwrap(); // Get project, as we need the gallery image url let req = test::TestRequest::get() - .uri(&format!("/v2/project/{alpha_project_id}")) + .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -755,7 +690,7 @@ async fn permissions_edit_details() { // Uses alpha project to edit gallery item let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch().uri(&format!( - "/v2/project/{}/gallery?url={gallery_url}", + "/v3/project/{}/gallery?url={gallery_url}", ctx.project_id.unwrap() )) }; @@ -770,7 +705,7 @@ async fn permissions_edit_details() { // Uses alpha project to remove gallery item let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/project/{}/gallery?url={gallery_url}", + "/v3/project/{}/gallery?url={gallery_url}", ctx.project_id.unwrap() )) }; @@ -794,7 +729,7 @@ async fn permissions_upload_version() { // Upload version with basic-mod.jar let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post().uri("/v2/version").set_multipart([ + test::TestRequest::post().uri("/v3/version").set_multipart([ MultipartSegment { name: "data".to_string(), filename: None, @@ -806,6 +741,8 @@ async fn permissions_upload_version() { "version_number": "1.0.0", "version_title": "1.0.0", "version_type": "release", + "client_side": "required", + "server_side": "optional", "dependencies": [], "game_versions": ["1.20.1"], "loaders": ["fabric"], @@ -834,7 +771,7 @@ async fn permissions_upload_version() { // Uses alpha project, as it has an existing version let req_gen = |_: &PermissionsTestContext| { test::TestRequest::post() - .uri(&format!("/v2/version/{}/file", alpha_version_id)) + .uri(&format!("/v3/version/{}/file", alpha_version_id)) .set_multipart([ MultipartSegment { name: "data".to_string(), @@ -868,7 +805,7 @@ async fn permissions_upload_version() { // Uses alpha project, as it has an existing version let req_gen = |_: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/version/{}", alpha_version_id)) + .uri(&format!("/v3/version/{}", alpha_version_id)) .set_json(json!({ "name": "Basic Mod", })) @@ -884,7 +821,7 @@ async fn permissions_upload_version() { // Uses alpha project, as it has an existing version let delete_version = ProjectPermissions::DELETE_VERSION; let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v2/version_file/{}", alpha_file_hash)) + test::TestRequest::delete().uri(&format!("/v3/version_file/{}", alpha_file_hash)) }; PermissionsTest::new(&test_env) @@ -897,7 +834,7 @@ async fn permissions_upload_version() { // Delete version // Uses alpha project, as it has an existing version let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v2/version/{}", alpha_version_id)) + test::TestRequest::delete().uri(&format!("/v3/version/{}", alpha_version_id)) }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -921,7 +858,7 @@ async fn permissions_manage_invites() { // Add member let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() - .uri(&format!("/v2/team/{}/members", ctx.team_id.unwrap())) + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) .set_json(json!({ "user_id": MOD_USER_ID, "permissions": 0, @@ -939,7 +876,7 @@ async fn permissions_manage_invites() { let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) .set_json(json!({ @@ -957,7 +894,7 @@ async fn permissions_manage_invites() { // requires manage_invites if they have not yet accepted the invite let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; @@ -970,7 +907,7 @@ async fn permissions_manage_invites() { // re-add member for testing let req = test::TestRequest::post() - .uri(&format!("/v2/team/{}/members", alpha_team_id)) + .uri(&format!("/v3/team/{}/members", alpha_team_id)) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "user_id": MOD_USER_ID, @@ -981,7 +918,7 @@ async fn permissions_manage_invites() { // Accept invite let req = test::TestRequest::post() - .uri(&format!("/v2/team/{}/join", alpha_team_id)) + .uri(&format!("/v3/team/{}/join", alpha_team_id)) .append_header(("Authorization", MOD_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -991,7 +928,7 @@ async fn permissions_manage_invites() { let remove_member = ProjectPermissions::REMOVE_MEMBER; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( - "/v2/team/{}/members/{MOD_USER_ID}", + "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; @@ -1015,7 +952,7 @@ async fn permissions_delete_project() { // Delete project let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + test::TestRequest::delete().uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) }; PermissionsTest::new(&test_env) .simple_project_permissions_test(delete_project, req_gen) @@ -1027,7 +964,7 @@ async fn permissions_delete_project() { #[actix_rt::test] async fn project_permissions_consistency_test() { - let test_env = TestEnvironment::build(Some(8)).await; + let test_env = TestEnvironment::build(Some(10)).await; // Test that the permissions are consistent with each other // For example, if we get the projectpermissions directly, from an organization's defaults, overriden, etc, they should all be correct & consistent @@ -1036,7 +973,7 @@ async fn project_permissions_consistency_test() { let success_permissions = ProjectPermissions::EDIT_DETAILS; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "title": "Example title - changed.", })) @@ -1053,7 +990,7 @@ async fn project_permissions_consistency_test() { | ProjectPermissions::VIEW_PAYOUTS; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "title": "Example title - changed.", })) diff --git a/tests/scopes.rs b/tests/scopes.rs index c0508e6b..e70f037f 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -1,21 +1,19 @@ use actix_web::test::{self, TestRequest}; use bytes::Bytes; use chrono::{Duration, Utc}; +use common::{database::*, environment::TestEnvironment, scopes::ScopeTest}; use labrinth::models::pats::Scopes; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; -use crate::common::{database::*, environment::TestEnvironment, scopes::ScopeTest}; - -// importing common module. -mod common; - // For each scope, we (using test_scope): // - create a PAT with a given set of scopes for a function // - create a PAT with all other scopes for a function // - test the function with the PAT with the given scopes // - test the function with the PAT with all other scopes +mod common; + // Test for users, emails, and payout scopes (not user auth scope or notifs) #[actix_rt::test] async fn user_scopes() { @@ -24,7 +22,7 @@ async fn user_scopes() { // User reading let read_user = Scopes::USER_READ; - let req_gen = || TestRequest::get().uri("/v2/user"); + let req_gen = || TestRequest::get().uri("/v3/user"); let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_user) .await @@ -34,7 +32,7 @@ async fn user_scopes() { // Email reading let read_email = Scopes::USER_READ | Scopes::USER_READ_EMAIL; - let req_gen = || TestRequest::get().uri("/v2/user"); + let req_gen = || TestRequest::get().uri("/v3/user"); let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_email) .await @@ -43,7 +41,7 @@ async fn user_scopes() { // Payout reading let read_payout = Scopes::USER_READ | Scopes::PAYOUTS_READ; - let req_gen = || TestRequest::get().uri("/v2/user"); + let req_gen = || TestRequest::get().uri("/v3/user"); let (_, success) = ScopeTest::new(&test_env) .test(req_gen, read_payout) .await @@ -54,7 +52,7 @@ async fn user_scopes() { // We use the Admin PAT for this test, on the 'user' user let write_user = Scopes::USER_WRITE; let req_gen = || { - TestRequest::patch().uri("/v2/user/user").set_json(json!( { + TestRequest::patch().uri("/v3/user/user").set_json(json!( { // Do not include 'username', as to not change the rest of the tests "name": "NewName", "bio": "New bio", @@ -73,7 +71,7 @@ async fn user_scopes() { // User deletion // (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests) let delete_user = Scopes::USER_DELETE; - let req_gen = || TestRequest::delete().uri("/v2/user/enemy"); + let req_gen = || TestRequest::delete().uri("/v3/user/enemy"); ScopeTest::new(&test_env) .with_user_id(ENEMY_USER_ID_PARSED) .test(req_gen, delete_user) @@ -99,7 +97,7 @@ pub async fn notifications_scopes() { // We will invite user 'friend' to project team, and use that as a notification // Get notifications let resp = test_env - .v2 + .v3 .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; assert_eq!(resp.status(), 204); @@ -107,7 +105,7 @@ pub async fn notifications_scopes() { // Notification get let read_notifications = Scopes::NOTIFICATION_READ; let req_gen = - || test::TestRequest::get().uri(&format!("/v2/user/{FRIEND_USER_ID}/notifications")); + || test::TestRequest::get().uri(&format!("/v3/user/{FRIEND_USER_ID}/notifications")); let (_, success) = ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) .test(req_gen, read_notifications) @@ -117,7 +115,7 @@ pub async fn notifications_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/notifications?ids=[{uri}]", + "/v3/notifications?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{notification_id}\"")) )) }; @@ -127,7 +125,7 @@ pub async fn notifications_scopes() { .await .unwrap(); - let req_gen = || test::TestRequest::get().uri(&format!("/v2/notification/{notification_id}")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/notification/{notification_id}")); ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) .test(req_gen, read_notifications) @@ -138,7 +136,7 @@ pub async fn notifications_scopes() { let write_notifications = Scopes::NOTIFICATION_WRITE; let req_gen = || { test::TestRequest::patch().uri(&format!( - "/v2/notifications?ids=[{uri}]", + "/v3/notifications?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{notification_id}\"")) )) }; @@ -148,7 +146,7 @@ pub async fn notifications_scopes() { .await .unwrap(); - let req_gen = || test::TestRequest::patch().uri(&format!("/v2/notification/{notification_id}")); + let req_gen = || test::TestRequest::patch().uri(&format!("/v3/notification/{notification_id}")); ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) .test(req_gen, write_notifications) @@ -157,7 +155,7 @@ pub async fn notifications_scopes() { // Notification delete let req_gen = - || test::TestRequest::delete().uri(&format!("/v2/notification/{notification_id}")); + || test::TestRequest::delete().uri(&format!("/v3/notification/{notification_id}")); ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) .test(req_gen, write_notifications) @@ -167,12 +165,12 @@ pub async fn notifications_scopes() { // Mass notification delete // We invite mod, get the notification ID, and do mass delete using that let resp = test_env - .v2 + .v3 .add_user_to_team(alpha_team_id, MOD_USER_ID, None, None, USER_USER_PAT) .await; assert_eq!(resp.status(), 204); let read_notifications = Scopes::NOTIFICATION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v2/user/{MOD_USER_ID}/notifications")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{MOD_USER_ID}/notifications")); let (_, success) = ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, read_notifications) @@ -182,7 +180,7 @@ pub async fn notifications_scopes() { let req_gen = || { test::TestRequest::delete().uri(&format!( - "/v2/notifications?ids=[{uri}]", + "/v3/notifications?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{notification_id}\"")) )) }; @@ -209,14 +207,14 @@ pub async fn project_version_create_scopes() { "slug": "demo", "description": "Example description.", "body": "Example body.", - "client_side": "required", - "server_side": "optional", "initial_versions": [{ "file_parts": ["basic-mod.jar"], "version_number": "1.2.3", "version_title": "start", "dependencies": [], "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", "release_channel": "release", "loaders": ["fabric"], "featured": true @@ -240,7 +238,7 @@ pub async fn project_version_create_scopes() { let req_gen = || { test::TestRequest::post() - .uri("/v2/project") + .uri("/v3/project") .set_multipart(vec![json_segment.clone(), file_segment.clone()]) }; let (_, success) = ScopeTest::new(&test_env) @@ -259,6 +257,8 @@ pub async fn project_version_create_scopes() { "version_title": "start", "dependencies": [], "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", "release_channel": "release", "loaders": ["fabric"], "featured": true @@ -281,7 +281,7 @@ pub async fn project_version_create_scopes() { let req_gen = || { test::TestRequest::post() - .uri("/v2/version") + .uri("/v3/version") .set_multipart(vec![json_segment.clone(), file_segment.clone()]) }; ScopeTest::new(&test_env) @@ -329,7 +329,7 @@ pub async fn project_version_reads_scopes() { // Project reading // Uses 404 as the expected failure code (or 200 and an empty list for mass reads) let read_project = Scopes::PROJECT_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v2/project/{beta_project_id}")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_project) @@ -337,7 +337,7 @@ pub async fn project_version_reads_scopes() { .unwrap(); let req_gen = - || test::TestRequest::get().uri(&format!("/v2/project/{beta_project_id}/dependencies")); + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/dependencies")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_project) @@ -346,7 +346,7 @@ pub async fn project_version_reads_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/projects?ids=[{uri}]", + "/v3/projects?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) )) }; @@ -360,7 +360,7 @@ pub async fn project_version_reads_scopes() { // Team project reading let req_gen = - || test::TestRequest::get().uri(&format!("/v2/project/{beta_project_id}/members")); + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/members")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_project) @@ -370,7 +370,7 @@ pub async fn project_version_reads_scopes() { // Get team members // In this case, as these are public endpoints, logging in only is relevant to showing permissions // So for our test project (with 1 user, 'user') we will check the permissions before and after having the scope. - let req_gen = || test::TestRequest::get().uri(&format!("/v2/team/{alpha_team_id}/members")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/team/{alpha_team_id}/members")); let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) .test(req_gen, read_project) @@ -381,7 +381,7 @@ pub async fn project_version_reads_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/teams?ids=[{uri}]", + "/v3/teams?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{alpha_team_id}\"")) )) }; @@ -395,7 +395,7 @@ pub async fn project_version_reads_scopes() { // User project reading // Test user has two projects, one public and one private - let req_gen = || test::TestRequest::get().uri(&format!("/v2/user/{USER_USER_ID}/projects")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/projects")); let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) .test(req_gen, read_project) @@ -428,7 +428,7 @@ pub async fn project_version_reads_scopes() { // First, set version to hidden (which is when the scope is required to read it) let read_version = Scopes::VERSION_READ; let req = test::TestRequest::patch() - .uri(&format!("/v2/version/{beta_version_id}")) + .uri(&format!("/v3/version/{beta_version_id}")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "status": "draft" @@ -437,7 +437,7 @@ pub async fn project_version_reads_scopes() { let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); - let req_gen = || test::TestRequest::get().uri(&format!("/v2/version_file/{beta_file_hash}")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_version) @@ -445,7 +445,7 @@ pub async fn project_version_reads_scopes() { .unwrap(); let req_gen = - || test::TestRequest::get().uri(&format!("/v2/version_file/{beta_file_hash}/download")); + || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}/download")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_version) @@ -456,7 +456,7 @@ pub async fn project_version_reads_scopes() { // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope // let req_gen = || { // test::TestRequest::post() - // .uri(&format!("/v2/version_file/{beta_file_hash}/update")) + // .uri(&format!("/v3/version_file/{beta_file_hash}/update")) // .set_json(json!({})) // }; // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_version).await.unwrap(); @@ -464,7 +464,7 @@ pub async fn project_version_reads_scopes() { // TODO: Should this be /POST? Looks like /GET let req_gen = || { test::TestRequest::post() - .uri("/v2/version_files") + .uri("/v3/version_files") .set_json(json!({ "hashes": [beta_file_hash] })) @@ -483,7 +483,7 @@ pub async fn project_version_reads_scopes() { // let req_gen = || { // test::TestRequest::post() - // .uri(&format!("/v2/version_files/update_individual")) + // .uri(&format!("/v3/version_files/update_individual")) // .set_json(json!({ // "hashes": [{ // "hash": beta_file_hash, @@ -498,7 +498,7 @@ pub async fn project_version_reads_scopes() { // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope // let req_gen = || { // test::TestRequest::post() - // .uri(&format!("/v2/version_files/update")) + // .uri(&format!("/v3/version_files/update")) // .set_json(json!({ // "hashes": [beta_file_hash] // })) @@ -510,7 +510,7 @@ pub async fn project_version_reads_scopes() { // Both project and version reading let read_project_and_version = Scopes::PROJECT_READ | Scopes::VERSION_READ; let req_gen = - || test::TestRequest::get().uri(&format!("/v2/project/{beta_project_id}/version")); + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/version")); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, read_project_and_version) @@ -520,7 +520,7 @@ pub async fn project_version_reads_scopes() { // TODO: fails for the same reason as above // let req_gen = || { // test::TestRequest::get() - // .uri(&format!("/v2/project/{beta_project_id}/version/{beta_version_id}")) + // .uri(&format!("/v3/project/{beta_project_id}/version/{beta_version_id}")) // }; // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_project_and_version).await.unwrap(); @@ -552,7 +552,7 @@ pub async fn project_write_scopes() { let write_project = Scopes::PROJECT_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/project/{beta_project_id}")) + .uri(&format!("/v3/project/{beta_project_id}")) .set_json(json!( { "title": "test_project_version_write_scopes Title", @@ -567,7 +567,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::patch() .uri(&format!( - "/v2/projects?ids=[{uri}]", + "/v3/projects?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) )) .set_json(json!( @@ -583,7 +583,7 @@ pub async fn project_write_scopes() { // Approve beta as private so we can schedule it let req = test::TestRequest::patch() - .uri(&format!("/v2/project/{beta_project_id}")) + .uri(&format!("/v3/project/{beta_project_id}")) .append_header(("Authorization", MOD_USER_PAT)) .set_json(json!({ "status": "private" @@ -594,7 +594,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/project/{beta_project_id}/schedule")) // beta_project_id is an unpublished can schedule it + .uri(&format!("/v3/project/{beta_project_id}/schedule")) // beta_project_id is an unpublished can schedule it .set_json(json!( { "requested_status": "private", @@ -610,7 +610,7 @@ pub async fn project_write_scopes() { // Icons and gallery images let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/project/{beta_project_id}/icon?ext=png")) + .uri(&format!("/v3/project/{beta_project_id}/icon?ext=png")) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] )) @@ -621,7 +621,7 @@ pub async fn project_write_scopes() { .unwrap(); let req_gen = - || test::TestRequest::delete().uri(&format!("/v2/project/{beta_project_id}/icon")); + || test::TestRequest::delete().uri(&format!("/v3/project/{beta_project_id}/icon")); ScopeTest::new(&test_env) .test(req_gen, write_project) .await @@ -630,7 +630,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::post() .uri(&format!( - "/v2/project/{beta_project_id}/gallery?ext=png&featured=true" + "/v3/project/{beta_project_id}/gallery?ext=png&featured=true" )) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] @@ -643,7 +643,7 @@ pub async fn project_write_scopes() { // Get project, as we need the gallery image url let req_gen = test::TestRequest::get() - .uri(&format!("/v2/project/{beta_project_id}")) + .uri(&format!("/v3/project/{beta_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req_gen).await; @@ -652,7 +652,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::patch().uri(&format!( - "/v2/project/{beta_project_id}/gallery?url={gallery_url}" + "/v3/project/{beta_project_id}/gallery?url={gallery_url}" )) }; ScopeTest::new(&test_env) @@ -662,7 +662,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::delete().uri(&format!( - "/v2/project/{beta_project_id}/gallery?url={gallery_url}" + "/v3/project/{beta_project_id}/gallery?url={gallery_url}" )) }; ScopeTest::new(&test_env) @@ -673,7 +673,7 @@ pub async fn project_write_scopes() { // Team scopes - add user 'friend' let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -684,7 +684,7 @@ pub async fn project_write_scopes() { .unwrap(); // Accept team invite as 'friend' - let req_gen = || test::TestRequest::post().uri(&format!("/v2/team/{alpha_team_id}/join")); + let req_gen = || test::TestRequest::post().uri(&format!("/v3/team/{alpha_team_id}/join")); ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) .test(req_gen, write_project) @@ -695,7 +695,7 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::patch() .uri(&format!( - "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .set_json(json!({ "permissions": 1 @@ -709,7 +709,7 @@ pub async fn project_write_scopes() { // Transfer ownership to 'friend' let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .uri(&format!("/v3/team/{alpha_team_id}/owner")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -721,7 +721,7 @@ pub async fn project_write_scopes() { // Now as 'friend', delete 'user' let req_gen = || { - test::TestRequest::delete().uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + test::TestRequest::delete().uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) }; ScopeTest::new(&test_env) .with_user_id(FRIEND_USER_ID_PARSED) @@ -736,7 +736,7 @@ pub async fn project_write_scopes() { // let delete_version = Scopes::PROJECT_DELETE; // let req_gen = || { // test::TestRequest::delete() - // .uri(&format!("/v2/project/{beta_project_id}")) + // .uri(&format!("/v3/project/{beta_project_id}")) // }; // ScopeTest::new(&test_env).test(req_gen, delete_version).await.unwrap(); @@ -775,7 +775,7 @@ pub async fn version_write_scopes() { // Approve beta version as private so we can schedule it let req = test::TestRequest::patch() - .uri(&format!("/v2/version/{beta_version_id}")) + .uri(&format!("/v3/version/{beta_version_id}")) .append_header(("Authorization", MOD_USER_PAT)) .set_json(json!({ "status": "unlisted" @@ -787,7 +787,7 @@ pub async fn version_write_scopes() { // Schedule version let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it + .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it .set_json(json!( { "requested_status": "archived", @@ -803,10 +803,10 @@ pub async fn version_write_scopes() { // Patch version let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/version/{alpha_version_id}")) + .uri(&format!("/v3/version/{alpha_version_id}")) .set_json(json!( { - "version_title": "test_version_write_scopes Title", + "name": "test_version_write_scopes Title", } )) }; @@ -846,7 +846,7 @@ pub async fn version_write_scopes() { // Upload version file let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/version/{alpha_version_id}/file")) + .uri(&format!("/v3/version/{alpha_version_id}/file")) .set_multipart(vec![json_segment.clone(), content_segment.clone()]) }; ScopeTest::new(&test_env) @@ -857,7 +857,7 @@ pub async fn version_write_scopes() { // Delete version file // TODO: Should this scope be VERSION_DELETE? let req_gen = || { - test::TestRequest::delete().uri(&format!("/v2/version_file/{alpha_file_hash}")) + test::TestRequest::delete().uri(&format!("/v3/version_file/{alpha_file_hash}")) // Delete from alpha_version_id, as we uploaded to alpha_version_id and it needs another file }; ScopeTest::new(&test_env) @@ -867,7 +867,7 @@ pub async fn version_write_scopes() { // Delete version let delete_version = Scopes::VERSION_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v2/version/{alpha_version_id}")); + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/version/{alpha_version_id}")); ScopeTest::new(&test_env) .test(req_gen, delete_version) .await @@ -893,7 +893,7 @@ pub async fn report_scopes() { // Create report let report_create = Scopes::REPORT_CREATE; let req_gen = || { - test::TestRequest::post().uri("/v2/report").set_json(json!({ + test::TestRequest::post().uri("/v3/report").set_json(json!({ "report_type": "copyright", "item_id": beta_project_id, "item_type": "project", @@ -907,14 +907,14 @@ pub async fn report_scopes() { // Get reports let report_read = Scopes::REPORT_READ; - let req_gen = || test::TestRequest::get().uri("/v2/report"); + let req_gen = || test::TestRequest::get().uri("/v3/report"); let (_, success) = ScopeTest::new(&test_env) .test(req_gen, report_read) .await .unwrap(); let report_id = success[0]["id"].as_str().unwrap(); - let req_gen = || test::TestRequest::get().uri(&format!("/v2/report/{}", report_id)); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/report/{}", report_id)); ScopeTest::new(&test_env) .test(req_gen, report_read) .await @@ -922,7 +922,7 @@ pub async fn report_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/reports?ids=[{}]", + "/v3/reports?ids=[{}]", urlencoding::encode(&format!("\"{}\"", report_id)) )) }; @@ -935,7 +935,7 @@ pub async fn report_scopes() { let report_edit = Scopes::REPORT_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/report/{}", report_id)) + .uri(&format!("/v3/report/{}", report_id)) .set_json(json!({ "body": "This is a reupload of my mod, G8!", })) @@ -948,7 +948,7 @@ pub async fn report_scopes() { // Delete report // We use a moderator PAT here, as only moderators can delete reports let report_delete = Scopes::REPORT_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v2/report/{}", report_id)); + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/report/{}", report_id)); ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, report_delete) @@ -981,7 +981,7 @@ pub async fn thread_scopes() { // Thread read let thread_read = Scopes::THREAD_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v2/thread/{alpha_thread_id}")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/thread/{alpha_thread_id}")); ScopeTest::new(&test_env) .test(req_gen, thread_read) .await @@ -989,7 +989,7 @@ pub async fn thread_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/threads?ids=[{}]", + "/v3/threads?ids=[{}]", urlencoding::encode(&format!("\"{}\"", "U")) )) }; @@ -1002,7 +1002,7 @@ pub async fn thread_scopes() { let thread_write = Scopes::THREAD_WRITE; let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/thread/{beta_thread_id}")) + .uri(&format!("/v3/thread/{beta_thread_id}")) .set_json(json!({ "body": { "type": "text", @@ -1018,17 +1018,17 @@ pub async fn thread_scopes() { // Check moderation inbox // Uses moderator PAT, as only moderators can see the moderation inbox - let req_gen = || test::TestRequest::get().uri("/v2/thread/inbox"); + let req_gen = || test::TestRequest::get().uri("/v3/thread/inbox"); let (_, success) = ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, thread_read) .await .unwrap(); - let thread_id = success[0]["id"].as_str().unwrap(); + let thread_id: &str = success[0]["id"].as_str().unwrap(); // Moderator 'read' thread // Uses moderator PAT, as only moderators can see the moderation inbox - let req_gen = || test::TestRequest::post().uri(&format!("/v2/thread/{thread_id}/read")); + let req_gen = || test::TestRequest::post().uri(&format!("/v3/thread/{thread_id}/read")); ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, thread_read) @@ -1038,14 +1038,14 @@ pub async fn thread_scopes() { // Delete that message // First, get message id let req_gen = test::TestRequest::get() - .uri(&format!("/v2/thread/{thread_id}")) + .uri(&format!("/v3/thread/{thread_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req_gen).await; let success: serde_json::Value = test::read_body_json(resp).await; let thread_message_id = success["messages"][0]["id"].as_str().unwrap(); - let req_gen = || test::TestRequest::delete().uri(&format!("/v2/message/{thread_message_id}")); + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/message/{thread_message_id}")); ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) .test(req_gen, thread_write) @@ -1064,7 +1064,7 @@ pub async fn pat_scopes() { // Pat create let pat_create = Scopes::PAT_CREATE; let req_gen = || { - test::TestRequest::post().uri("/v2/pat").set_json(json!({ + test::TestRequest::post().uri("/v3/pat").set_json(json!({ "scopes": 1, "name": "test_pat_scopes Name", "expires": Utc::now() + Duration::days(1), @@ -1080,7 +1080,7 @@ pub async fn pat_scopes() { let pat_write = Scopes::PAT_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/pat/{pat_id}")) + .uri(&format!("/v3/pat/{pat_id}")) .set_json(json!({})) }; ScopeTest::new(&test_env) @@ -1090,7 +1090,7 @@ pub async fn pat_scopes() { // Pat read let pat_read = Scopes::PAT_READ; - let req_gen = || test::TestRequest::get().uri("/v2/pat"); + let req_gen = || test::TestRequest::get().uri("/v3/pat"); ScopeTest::new(&test_env) .test(req_gen, pat_read) .await @@ -1098,7 +1098,7 @@ pub async fn pat_scopes() { // Pat delete let pat_delete = Scopes::PAT_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v2/pat/{pat_id}")); + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/pat/{pat_id}")); ScopeTest::new(&test_env) .test(req_gen, pat_delete) .await @@ -1125,7 +1125,7 @@ pub async fn collections_scopes() { let collection_create = Scopes::COLLECTION_CREATE; let req_gen = || { test::TestRequest::post() - .uri("/v2/collection") + .uri("/v3/collection") .set_json(json!({ "title": "Test Collection", "description": "Test Collection Description", @@ -1143,7 +1143,7 @@ pub async fn collections_scopes() { let collection_write = Scopes::COLLECTION_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/collection/{collection_id}")) + .uri(&format!("/v3/collection/{collection_id}")) .set_json(json!({ "title": "Test Collection patch", "status": "private", @@ -1156,7 +1156,7 @@ pub async fn collections_scopes() { // Read collection let collection_read = Scopes::COLLECTION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v2/collection/{}", collection_id)); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/collection/{}", collection_id)); ScopeTest::new(&test_env) .with_failure_code(404) .test(req_gen, collection_read) @@ -1165,7 +1165,7 @@ pub async fn collections_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/collections?ids=[{}]", + "/v3/collections?ids=[{}]", urlencoding::encode(&format!("\"{}\"", collection_id)) )) }; @@ -1177,7 +1177,7 @@ pub async fn collections_scopes() { assert_eq!(failure.as_array().unwrap().len(), 0); assert_eq!(success.as_array().unwrap().len(), 1); - let req_gen = || test::TestRequest::get().uri(&format!("/v2/user/{USER_USER_ID}/collections")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/collections")); let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) .test(req_gen, collection_read) @@ -1188,7 +1188,7 @@ pub async fn collections_scopes() { let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/collection/{collection_id}/icon?ext=png")) + .uri(&format!("/v3/collection/{collection_id}/icon?ext=png")) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] )) @@ -1199,7 +1199,7 @@ pub async fn collections_scopes() { .unwrap(); let req_gen = - || test::TestRequest::delete().uri(&format!("/v2/collection/{collection_id}/icon")); + || test::TestRequest::delete().uri(&format!("/v3/collection/{collection_id}/icon")); ScopeTest::new(&test_env) .test(req_gen, collection_write) .await @@ -1226,7 +1226,7 @@ pub async fn organization_scopes() { let organization_create = Scopes::ORGANIZATION_CREATE; let req_gen = || { test::TestRequest::post() - .uri("/v2/organization") + .uri("/v3/organization") .set_json(json!({ "title": "TestOrg", "description": "TestOrg Description", @@ -1242,7 +1242,7 @@ pub async fn organization_scopes() { let organization_edit = Scopes::ORGANIZATION_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/organization/{organization_id}")) + .uri(&format!("/v3/organization/{organization_id}")) .set_json(json!({ "description": "TestOrg Patch Description", })) @@ -1254,7 +1254,7 @@ pub async fn organization_scopes() { let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v2/organization/{organization_id}/icon?ext=png")) + .uri(&format!("/v3/organization/{organization_id}/icon?ext=png")) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] )) @@ -1265,7 +1265,7 @@ pub async fn organization_scopes() { .unwrap(); let req_gen = - || test::TestRequest::delete().uri(&format!("/v2/organization/{organization_id}/icon")); + || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}/icon")); ScopeTest::new(&test_env) .test(req_gen, organization_edit) .await @@ -1275,7 +1275,7 @@ pub async fn organization_scopes() { let organization_project_edit = Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE; let req_gen = || { test::TestRequest::post() - .uri(&format!("/v2/organization/{organization_id}/projects")) + .uri(&format!("/v3/organization/{organization_id}/projects")) .set_json(json!({ "project_id": beta_project_id })) @@ -1288,7 +1288,7 @@ pub async fn organization_scopes() { // Organization reads let organization_read = Scopes::ORGANIZATION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v2/organization/{organization_id}")); + let req_gen = || test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}")); let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) .test(req_gen, organization_read) @@ -1299,7 +1299,7 @@ pub async fn organization_scopes() { let req_gen = || { test::TestRequest::get().uri(&format!( - "/v2/organizations?ids=[{}]", + "/v3/organizations?ids=[{}]", urlencoding::encode(&format!("\"{}\"", organization_id)) )) }; @@ -1314,7 +1314,7 @@ pub async fn organization_scopes() { let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; let req_gen = - || test::TestRequest::get().uri(&format!("/v2/organization/{organization_id}/projects")); + || test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}/projects")); let (failure, success) = ScopeTest::new(&test_env) .with_failure_code(200) .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_READ) @@ -1327,7 +1327,7 @@ pub async fn organization_scopes() { // remove project (now that we've checked) let req_gen = || { test::TestRequest::delete().uri(&format!( - "/v2/organization/{organization_id}/projects/{beta_project_id}" + "/v3/organization/{organization_id}/projects/{beta_project_id}" )) }; ScopeTest::new(&test_env) @@ -1339,7 +1339,7 @@ pub async fn organization_scopes() { // Delete organization let organization_delete = Scopes::ORGANIZATION_DELETE; let req_gen = - || test::TestRequest::delete().uri(&format!("/v2/organization/{organization_id}")); + || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}")); ScopeTest::new(&test_env) .test(req_gen, organization_delete) .await diff --git a/tests/search.rs b/tests/search.rs index 9e87ee18..36483547 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -1,23 +1,27 @@ -use crate::common::database::*; -use crate::common::dummy_data::DUMMY_CATEGORIES; -use crate::common::environment::TestEnvironment; -use crate::common::request_data::{get_public_version_creation_data, ProjectCreationRequestData}; +use common::database::*; use common::dummy_data::TestFile; -use common::request_data; +use common::dummy_data::DUMMY_CATEGORIES; +use common::environment::TestEnvironment; use futures::stream::StreamExt; use labrinth::models::ids::base62_impl::parse_base62; use serde_json::json; use std::collections::HashMap; use std::sync::Arc; -// importing common module. +use crate::common::api_v3::request_data::{ + self, get_public_version_creation_data, ProjectCreationRequestData, +}; + mod common; +// TODO: Revisit this with the new modify_json in the version maker +// That change here should be able to simplify it vastly + #[actix_rt::test] async fn search_projects() { // Test setup and dummy data let test_env = TestEnvironment::build(Some(8)).await; - let api = &test_env.v2; + let api = &test_env.v3; let test_name = test_env.db.database_name.clone(); // Add dummy projects of various categories for searchability @@ -38,7 +42,6 @@ async fn search_projects() { let mut basic_project_json = request_data::get_public_project_creation_data_json(&slug, Some(&jar)); modify_json(&mut basic_project_json); - let basic_project_multipart = request_data::get_public_creation_data_multipart(&basic_project_json, Some(&jar)); // Add a project- simple, should work. @@ -72,7 +75,7 @@ async fn search_projects() { let id = 0; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[4..6]); - json["server_side"] = json!("required"); + json["initial_versions"][0]["server_side"] = json!("required"); json["license_id"] = json!("LGPL-3.0-or-later"); }; project_creation_futures.push(create_async_future( @@ -86,7 +89,7 @@ async fn search_projects() { let id = 1; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["client_side"] = json!("optional"); + json["initial_versions"][0]["client_side"] = json!("optional"); }; project_creation_futures.push(create_async_future( id, @@ -99,7 +102,7 @@ async fn search_projects() { let id = 2; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["server_side"] = json!("required"); + json["initial_versions"][0]["server_side"] = json!("required"); json["title"] = json!("Mysterious Project"); }; project_creation_futures.push(create_async_future( @@ -113,7 +116,7 @@ async fn search_projects() { let id = 3; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["server_side"] = json!("required"); + json["initial_versions"][0]["server_side"] = json!("required"); json["initial_versions"][0]["game_versions"] = json!(["1.20.4"]); json["title"] = json!("Mysterious Project"); json["license_id"] = json!("LicenseRef-All-Rights-Reserved"); // closed source @@ -129,7 +132,7 @@ async fn search_projects() { let id = 4; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["client_side"] = json!("optional"); + json["initial_versions"][0]["client_side"] = json!("optional"); json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); }; project_creation_futures.push(create_async_future( @@ -143,7 +146,7 @@ async fn search_projects() { let id = 5; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); + json["initial_versions"][0]["client_side"] = json!("optional"); json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); json["license_id"] = json!("LGPL-3.0-or-later"); }; @@ -158,8 +161,8 @@ async fn search_projects() { let id = 6; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); - json["server_side"] = json!("required"); + json["initial_versions"][0]["client_side"] = json!("optional"); + json["initial_versions"][0]["server_side"] = json!("required"); json["license_id"] = json!("LGPL-3.0-or-later"); }; project_creation_futures.push(create_async_future( @@ -175,8 +178,8 @@ async fn search_projects() { let id = 7; let modify_json = |json: &mut serde_json::Value| { json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); - json["server_side"] = json!("required"); + json["initial_versions"][0]["client_side"] = json!("optional"); + json["initial_versions"][0]["server_side"] = json!("required"); json["license_id"] = json!("LGPL-3.0-or-later"); json["initial_versions"][0]["loaders"] = json!(["forge"]); json["initial_versions"][0]["game_versions"] = json!(["1.20.2"]); @@ -203,7 +206,12 @@ async fn search_projects() { .get_project_deserialized(&format!("{test_name}-searchable-project-7"), USER_USER_PAT) .await; api.add_public_version( - get_public_version_creation_data(project_7.id, "1.0.0", TestFile::build_random_jar()), + get_public_version_creation_data( + project_7.id, + "1.0.0", + TestFile::build_random_jar(), + None::, + ), USER_USER_PAT, ) .await; @@ -226,21 +234,21 @@ async fn search_projects() { ]), vec![1, 2, 3, 4], ), - (json!([["project_type:modpack"]]), vec![4]), - (json!([["client_side:required"]]), vec![0, 2, 3]), + (json!([["project_types:modpack"]]), vec![4]), + (json!([["client_side:required"]]), vec![0, 2, 3, 7]), (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), (json!([["license:MIT"]]), vec![1, 2, 4]), (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), - (json!([["versions:1.20.5"]]), vec![4, 5]), + (json!([["game_versions:1.20.5"]]), vec![4, 5]), // bug fix ( json!([ // Only the forge one has 1.20.2, so its true that this project 'has' // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. ["categories:fabric"], - ["versions:1.20.2"] + ["game_versions:1.20.2"] ]), vec![], ), @@ -255,7 +263,7 @@ async fn search_projects() { json!([ ["categories:mrpack"], ["categories:fabric"], - ["project_type:modpack"] + ["project_types:modpack"] ]), vec![4], ), @@ -266,6 +274,7 @@ async fn search_projects() { // - color (not varied) // - created_timestamp (not varied) // - modified_timestamp (not varied) + // TODO: multiple different project types test // Forcibly reset the search index let resp = api.reset_search_index().await; diff --git a/tests/tags.rs b/tests/tags.rs index 5c9109ed..3b986a3d 100644 --- a/tests/tags.rs +++ b/tests/tags.rs @@ -1,6 +1,4 @@ -use itertools::Itertools; - -use crate::common::environment::TestEnvironment; +use common::environment::TestEnvironment; use std::collections::HashSet; mod common; @@ -8,40 +6,11 @@ mod common; #[actix_rt::test] async fn get_tags() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; - let game_versions = api.get_game_versions_deserialized().await; let loaders = api.get_loaders_deserialized().await; - let side_types = api.get_side_types_deserialized().await; let categories = api.get_categories_deserialized().await; - // These tests match dummy data and will need to be updated if the dummy data changes - // Versions should be ordered by: - // - ordering - // - ordering ties settled by date added to database - // - We also expect presentation of NEWEST to OLDEST - // - All null orderings are treated as older than any non-null ordering - // (for this test, the 1.20.1, etc, versions are all null ordering) - let game_version_versions = game_versions - .into_iter() - .map(|x| x.version) - .collect::>(); - assert_eq!( - game_version_versions, - [ - "Ordering_Negative1", - "Ordering_Positive100", - "1.20.5", - "1.20.4", - "1.20.3", - "1.20.2", - "1.20.1" - ] - .iter() - .map(|s| s.to_string()) - .collect_vec() - ); - let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); assert_eq!( loader_names, @@ -51,15 +20,6 @@ async fn get_tags() { .collect() ); - let side_type_names = side_types.into_iter().collect::>(); - assert_eq!( - side_type_names, - ["unknown", "required", "optional", "unsupported"] - .iter() - .map(|s| s.to_string()) - .collect() - ); - let category_names = categories .into_iter() .map(|x| x.name) diff --git a/tests/teams.rs b/tests/teams.rs index 621b9eec..74a1d630 100644 --- a/tests/teams.rs +++ b/tests/teams.rs @@ -1,12 +1,9 @@ +use crate::common::database::*; +use crate::common::environment::TestEnvironment; use actix_web::test; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; -use crate::common::database::*; - -use crate::common::environment::TestEnvironment; - -// importing common module. mod common; #[actix_rt::test] @@ -30,8 +27,8 @@ async fn test_get_team() { ] { // A non-member of the team should get basic info but not be able to see private data for uri in [ - format!("/v2/team/{team_id}/members"), - format!("/v2/{team_association}/{team_association_id}/members"), + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), ] { let req = test::TestRequest::get() .uri(&uri) @@ -49,7 +46,7 @@ async fn test_get_team() { // - not be able to see private data about the team, but see all members including themselves // - should not appear in the team members list to enemy users let req = test::TestRequest::post() - .uri(&format!("/v2/team/{team_id}/members")) + .uri(&format!("/v3/team/{team_id}/members")) .append_header(("Authorization", USER_USER_PAT)) .set_json(&json!({ "user_id": FRIEND_USER_ID, @@ -59,8 +56,8 @@ async fn test_get_team() { assert_eq!(resp.status(), 204); for uri in [ - format!("/v2/team/{team_id}/members"), - format!("/v2/{team_association}/{team_association_id}/members"), + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), ] { let req = test::TestRequest::get() .uri(&uri) @@ -99,15 +96,15 @@ async fn test_get_team() { // An accepted member of the team should appear in the team members list // and should be able to see private data about the team let req = test::TestRequest::post() - .uri(&format!("/v2/team/{team_id}/join")) + .uri(&format!("/v3/team/{team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); for uri in [ - format!("/v2/team/{team_id}/members"), - format!("/v2/{team_association}/{team_association_id}/members"), + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), ] { let req = test::TestRequest::get() .uri(&uri) @@ -153,7 +150,7 @@ async fn test_get_team_project_orgs() { // Attach alpha to zeta let req = test::TestRequest::post() - .uri(&format!("/v2/organization/{zeta_organization_id}/projects")) + .uri(&format!("/v3/organization/{zeta_organization_id}/projects")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "project_id": alpha_project_id, @@ -164,7 +161,7 @@ async fn test_get_team_project_orgs() { // Invite and add friend to zeta let req = test::TestRequest::post() - .uri(&format!("/v2/team/{zeta_team_id}/members")) + .uri(&format!("/v3/team/{zeta_team_id}/members")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "user_id": FRIEND_USER_ID, @@ -174,7 +171,7 @@ async fn test_get_team_project_orgs() { assert_eq!(resp.status(), 204); let req = test::TestRequest::post() - .uri(&format!("/v2/team/{zeta_team_id}/join")) + .uri(&format!("/v3/team/{zeta_team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -184,7 +181,7 @@ async fn test_get_team_project_orgs() { // - the members of the project team specifically // - not the ones from the organization let req = test::TestRequest::get() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -196,7 +193,7 @@ async fn test_get_team_project_orgs() { // The team members route from project should show: // - the members of the project team including the ones from the organization let req = test::TestRequest::get() - .uri(&format!("/v2/project/{alpha_project_id}/members")) + .uri(&format!("/v3/project/{alpha_project_id}/members")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -218,7 +215,7 @@ async fn test_patch_project_team_member() { // Edit team as admin/mod but not a part of the team should be OK let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) .set_json(json!({})) .append_header(("Authorization", ADMIN_USER_PAT)) .to_request(); @@ -227,7 +224,7 @@ async fn test_patch_project_team_member() { // As a non-owner with full permissions, attempt to edit the owner's permissions/roles let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "role": "member" @@ -237,7 +234,7 @@ async fn test_patch_project_team_member() { assert_eq!(resp.status(), 400); let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "permissions": 0 @@ -249,7 +246,7 @@ async fn test_patch_project_team_member() { // Should not be able to edit organization permissions of a project team let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "organization_permissions": 0 @@ -264,7 +261,7 @@ async fn test_patch_project_team_member() { // first, invite friend let req = test::TestRequest::post() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "user_id": FRIEND_USER_ID, @@ -276,7 +273,7 @@ async fn test_patch_project_team_member() { // accept let req = test::TestRequest::post() - .uri(&format!("/v2/team/{alpha_team_id}/join")) + .uri(&format!("/v3/team/{alpha_team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -284,7 +281,7 @@ async fn test_patch_project_team_member() { // try to add permissions let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}")) + .uri(&format!("/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", FRIEND_USER_PAT)) .set_json(json!({ "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits() @@ -296,7 +293,7 @@ async fn test_patch_project_team_member() { // Cannot set a user to Owner let req = test::TestRequest::patch() .uri(&format!( - "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ @@ -310,7 +307,7 @@ async fn test_patch_project_team_member() { for payout in [-1, 5001] { let req = test::TestRequest::patch() .uri(&format!( - "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ @@ -325,7 +322,7 @@ async fn test_patch_project_team_member() { // Successful patch let req = test::TestRequest::patch() .uri(&format!( - "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .append_header(("Authorization", FRIEND_USER_PAT)) .set_json(json!({ @@ -340,7 +337,7 @@ async fn test_patch_project_team_member() { // Check results let req = test::TestRequest::get() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -373,7 +370,7 @@ async fn test_patch_organization_team_member() { // Edit team as admin/mod but not a part of the team should be OK let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) .set_json(json!({})) .append_header(("Authorization", ADMIN_USER_PAT)) .to_request(); @@ -382,7 +379,7 @@ async fn test_patch_organization_team_member() { // As a non-owner with full permissions, attempt to edit the owner's permissions/roles let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "role": "member" @@ -392,7 +389,7 @@ async fn test_patch_organization_team_member() { assert_eq!(resp.status(), 400); let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "permissions": 0 @@ -406,7 +403,7 @@ async fn test_patch_organization_team_member() { // first, invite friend let req = test::TestRequest::post() - .uri(&format!("/v2/team/{zeta_team_id}/members")) + .uri(&format!("/v3/team/{zeta_team_id}/members")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "user_id": FRIEND_USER_ID, @@ -417,7 +414,7 @@ async fn test_patch_organization_team_member() { // accept let req = test::TestRequest::post() - .uri(&format!("/v2/team/{zeta_team_id}/join")) + .uri(&format!("/v3/team/{zeta_team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -425,7 +422,7 @@ async fn test_patch_organization_team_member() { // try to add permissions- fails, as we do not have EDIT_DETAILS let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", FRIEND_USER_PAT)) .set_json(json!({ "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() @@ -437,7 +434,7 @@ async fn test_patch_organization_team_member() { // Cannot set a user to Owner let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "role": "Owner" @@ -450,7 +447,7 @@ async fn test_patch_organization_team_member() { // Cannot set payouts outside of 0 and 5000 for payout in [-1, 5001] { let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "payouts_split": payout @@ -462,7 +459,7 @@ async fn test_patch_organization_team_member() { // Successful patch let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", FRIEND_USER_PAT)) .set_json(json!({ "payouts_split": 51, @@ -478,7 +475,7 @@ async fn test_patch_organization_team_member() { // Check results let req = test::TestRequest::get() - .uri(&format!("/v2/team/{zeta_team_id}/members")) + .uri(&format!("/v3/team/{zeta_team_id}/members")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -515,7 +512,7 @@ async fn transfer_ownership() { // Cannot set friend as owner (not a member) let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .uri(&format!("/v3/team/{alpha_team_id}/owner")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -526,7 +523,7 @@ async fn transfer_ownership() { // first, invite friend let req = test::TestRequest::post() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "user_id": FRIEND_USER_ID, @@ -537,7 +534,7 @@ async fn transfer_ownership() { // accept let req = test::TestRequest::post() - .uri(&format!("/v2/team/{alpha_team_id}/join")) + .uri(&format!("/v3/team/{alpha_team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; @@ -545,7 +542,7 @@ async fn transfer_ownership() { // Cannot set ourselves as owner let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .uri(&format!("/v3/team/{alpha_team_id}/owner")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -556,7 +553,7 @@ async fn transfer_ownership() { // Can set friend as owner let req = test::TestRequest::patch() - .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .uri(&format!("/v3/team/{alpha_team_id}/owner")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -567,7 +564,7 @@ async fn transfer_ownership() { // Check let req = test::TestRequest::get() - .uri(&format!("/v2/team/{alpha_team_id}/members")) + .uri(&format!("/v3/team/{alpha_team_id}/members")) .set_json(json!({ "user_id": FRIEND_USER_ID })) @@ -599,7 +596,7 @@ async fn transfer_ownership() { // Confirm that user, a user who still has full permissions, cannot then remove the owner let req = test::TestRequest::delete() .uri(&format!( - "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .append_header(("Authorization", USER_USER_PAT)) .to_request(); @@ -620,7 +617,7 @@ async fn transfer_ownership() { // // This is because project-team permission overrriding must be possible, and this overriding can decrease the number of permissions a user has. // let test_env = TestEnvironment::build(None).await; -// let api = &test_env.v2; +// let api = &test_env.v3; // let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; // let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; diff --git a/tests/user.rs b/tests/user.rs index 82fcc3f8..75a97b83 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -3,7 +3,8 @@ use common::{ environment::with_test_environment, }; -use crate::common::{dummy_data::TestFile, request_data::get_public_project_creation_data}; +use crate::common::api_v3::request_data::get_public_project_creation_data; +use common::dummy_data::TestFile; mod common; @@ -19,7 +20,7 @@ mod common; #[actix_rt::test] pub async fn get_user_projects_after_creating_project_returns_new_project() { with_test_environment(|test_env| async move { - let api = test_env.v2; + let api = test_env.v3; api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) .await; @@ -41,7 +42,7 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() { #[actix_rt::test] pub async fn get_user_projects_after_deleting_project_shows_removal() { with_test_environment(|test_env| async move { - let api = test_env.v2; + let api = test_env.v3; let (project, _) = api .add_public_project( get_public_project_creation_data("iota", Some(TestFile::BasicMod)), @@ -67,7 +68,7 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() { with_test_environment(|test_env| async move { let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let api = test_env.v2; + let api = test_env.v3; api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; @@ -90,7 +91,7 @@ pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() { with_test_environment(|test_env| async move { let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let api = test_env.v2; + let api = test_env.v3; api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; api.join_team(alpha_team_id, FRIEND_USER_PAT).await; diff --git a/tests/v2/project.rs b/tests/v2/project.rs new file mode 100644 index 00000000..609b8481 --- /dev/null +++ b/tests/v2/project.rs @@ -0,0 +1,538 @@ +use crate::common::{ + api_v2::request_data, + database::{ENEMY_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, MOD_USER_PAT, USER_USER_PAT}, + dummy_data::{TestFile, DUMMY_CATEGORIES}, + environment::TestEnvironment, + permissions::{PermissionsTest, PermissionsTestContext}, +}; +use actix_web::test; +use itertools::Itertools; +use labrinth::{ + database::models::project_item::PROJECTS_SLUGS_NAMESPACE, + models::teams::ProjectPermissions, + util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}, +}; +use serde_json::json; + +#[actix_rt::test] +async fn test_project_type_sanity() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + // Perform all other patch tests on both 'mod' and 'modpack' + let test_creation_mod = request_data::get_public_project_creation_data( + "test-mod", + Some(TestFile::build_random_jar()), + ); + let test_creation_modpack = request_data::get_public_project_creation_data( + "test-modpack", + Some(TestFile::build_random_mrpack()), + ); + for (mod_or_modpack, test_creation_data) in [ + ("mod", test_creation_mod), + ("modpack", test_creation_modpack), + ] { + let (test_project, test_version) = api + .add_public_project(test_creation_data, USER_USER_PAT) + .await; + let test_project_slug = test_project.slug.as_ref().unwrap(); + + assert_eq!(test_project.project_type, mod_or_modpack); + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!( + test_version[0].loaders.iter().map(|x| &x.0).collect_vec(), + vec!["fabric"] + ); + + let project = api + .get_project_deserialized(test_project_slug, USER_USER_PAT) + .await; + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!(project.project_type, mod_or_modpack); + + let version = api + .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) + .await; + assert_eq!( + version.loaders.iter().map(|x| &x.0).collect_vec(), + vec!["fabric"] + ); + } + + // TODO: as we get more complicated strucures with v3 testing, and alpha/beta get more complicated, we should add more tests here, + // to ensure that projects created with v3 routes are still valid and work with v3 routes. +} + +#[actix_rt::test] +async fn test_add_remove_project() { + // Test setup and dummy data + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + // Generate test project data. + let mut json_data = json!( + { + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "client_side": "required", + "server_side": "optional", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" + } + ); + + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + + // Basic json, with a different file + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic json, with a different file, and a different slug + json_data["slug"] = json!("new_demo"); + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_slug_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic file + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + // TODO: look at these: can be simplified with TestFile + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with the same content (for hash testing) + let file_diff_name_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with different content + let file_diff_name_content_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + // Add a project- simple, should work. + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .to_request(); + let resp = test_env.call(req).await; + + let status = resp.status(); + assert_eq!(status, 200); + + // Get the project we just made, and confirm that it's correct + let project = api.get_project_deserialized("demo", USER_USER_PAT).await; + assert!(project.versions.len() == 1); + let uploaded_version_id = project.versions[0]; + + // Checks files to ensure they were uploaded and correctly identify the file + let hash = sha1::Sha1::from(include_bytes!("../../tests/files/basic-mod.jar")) + .digest() + .to_string(); + let version = api + .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) + .await; + assert_eq!(version.id, uploaded_version_id); + + // Reusing with a different slug and the same file should fail + // Even if that file is named differently + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Reusing with the same slug and a different file should fail + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_file_segment.clone(), // Same slug, different file name + file_diff_name_content_segment.clone(), // Different file name, different content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Different slug, different file should succeed + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_content_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + + // Get + let project = api.get_project_deserialized("demo", USER_USER_PAT).await; + let id = project.id.to_string(); + + // Remove the project + let resp = test_env.v2.remove_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // Confirm that the project is gone from the cache + assert_eq!( + test_env + .db + .redis_pool + .get::(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap(), + None + ); + assert_eq!( + test_env + .db + .redis_pool + .get::(PROJECTS_SLUGS_NAMESPACE, id) + .await + .unwrap(), + None + ); + + // Old slug no longer works + let resp = api.get_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + + // Cleanup test db + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_upload_version() { + let test_env = TestEnvironment::build(None).await; + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + + let upload_version = ProjectPermissions::UPLOAD_VERSION; + + // Upload version with basic-mod.jar + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post().uri("/v2/version").set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json!({ + "project_id": ctx.project_id.unwrap(), + "file_parts": ["basic-mod.jar"], + "version_number": "1.0.0", + "version_title": "1.0.0", + "version_type": "release", + "dependencies": [], + "game_versions": ["1.20.1"], + "loaders": ["fabric"], + "featured": false, + + })) + .unwrap(), + ), + }, + MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }, + ]) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Upload file to existing version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v2/version/{}/file", alpha_version_id)) + .set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json!({ + "file_parts": ["basic-mod-different.jar"], + })) + .unwrap(), + ), + }, + MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }, + ]) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Patch version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v2/version/{}", alpha_version_id)) + .set_json(json!({ + "name": "Basic Mod", + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Delete version file + // Uses alpha project, as it has an existing version + let delete_version = ProjectPermissions::DELETE_VERSION; + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v2/version_file/{}", alpha_file_hash)) + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + + // Delete version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v2/version/{}", alpha_version_id)) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} + +#[actix_rt::test] +pub async fn test_patch_project() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; + + // First, we do some patch requests that should fail. + // Failure because the user is not authorized. + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "title": "Test_Add_Project project - test 1", + }), + ENEMY_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // Failure because we are setting URL fields to invalid urls. + for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + url_type: "w.fake.url", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "processing", "withheld", "scheduled"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "requested_status": req, + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failure because these should not be able to be set by a non-mod + for key in ["moderation_message", "moderation_message_body"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // (should work for a mod, though) + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + } + + // Failed patch to alpha slug: + // - slug collision with beta + // - too short slug + // - too long slug + // - not url safe slug + // - not url safe slug + for slug in [ + beta_project_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "slug": slug, // the other dummy project has this slug + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. + let resp = api + .edit_project( + beta_project_slug, + json!({ + "status": "private" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // Sucessful request to patch many fields. + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "slug": "newslug", + "title": "New successful title", + "description": "New successful description", + "body": "New successful body", + "categories": [DUMMY_CATEGORIES[0]], + "license_id": "MIT", + "issues_url": "https://github.com", + "discord_url": "https://discord.gg", + "wiki_url": "https://wiki.com", + "client_side": "optional", + "server_side": "required", + "donation_urls": [{ + "id": "patreon", + "platform": "Patreon", + "url": "https://patreon.com" + }] + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + // Old slug no longer works + let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + + // New slug does work + let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; + + assert_eq!(project.slug.unwrap(), "newslug"); + assert_eq!(project.title, "New successful title"); + assert_eq!(project.description, "New successful description"); + assert_eq!(project.body, "New successful body"); + assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); + assert_eq!(project.license.id, "MIT"); + assert_eq!(project.issues_url, Some("https://github.com".to_string())); + assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); + assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); + assert_eq!(project.client_side.as_str(), "optional"); + assert_eq!(project.server_side.as_str(), "required"); + assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); + + // Cleanup test db + test_env.cleanup().await; +} diff --git a/tests/v2/scopes.rs b/tests/v2/scopes.rs new file mode 100644 index 00000000..acb877f4 --- /dev/null +++ b/tests/v2/scopes.rs @@ -0,0 +1,109 @@ +use crate::common::environment::TestEnvironment; +use crate::common::scopes::ScopeTest; +use actix_web::test; +use labrinth::models::pats::Scopes; +use labrinth::util::actix::AppendsMultipart; +use labrinth::util::actix::MultipartSegment; +use labrinth::util::actix::MultipartSegmentData; + +use serde_json::json; +// Project version creation scopes +#[actix_rt::test] +pub async fn project_version_create_scopes() { + let test_env = TestEnvironment::build(None).await; + + // Create project + let create_project = Scopes::PROJECT_CREATE; + let json_data = json!( + { + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" + } + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v3/project") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); + + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let json_data = json!( + { + "project_id": project_id, + "file_parts": ["basic-mod-different.jar"], + "version_number": "1.2.3.4", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + } + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v3/version") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + + // Cleanup test db + test_env.cleanup().await; +} diff --git a/tests/v2/search.rs b/tests/v2/search.rs new file mode 100644 index 00000000..fbe39ca6 --- /dev/null +++ b/tests/v2/search.rs @@ -0,0 +1,299 @@ +use crate::common::api_v2::request_data; +use crate::common::api_v2::request_data::get_public_version_creation_data; +use crate::common::api_v2::request_data::ProjectCreationRequestData; +use crate::common::database::*; +use crate::common::dummy_data::TestFile; +use crate::common::dummy_data::DUMMY_CATEGORIES; +use crate::common::environment::TestEnvironment; +use futures::stream::StreamExt; +use labrinth::models::ids::base62_impl::parse_base62; +use serde_json::json; +use std::collections::HashMap; +use std::sync::Arc; + +#[actix_rt::test] +async fn search_projects() { + // TODO: ("Match changes in the 2 version of thee add_public_version_creation_data to those made in v3 + // It should drastically simplify this function + + // Test setup and dummy data + let test_env = TestEnvironment::build(Some(8)).await; + let api = &test_env.v2; + let test_name = test_env.db.database_name.clone(); + + // Add dummy projects of various categories for searchability + let mut project_creation_futures = vec![]; + + let create_async_future = + |id: u64, + pat: &'static str, + is_modpack: bool, + modify_json: Box| { + let slug = format!("{test_name}-searchable-project-{id}"); + + let jar = if is_modpack { + TestFile::build_random_mrpack() + } else { + TestFile::build_random_jar() + }; + let mut basic_project_json = + request_data::get_public_project_creation_data_json(&slug, Some(&jar)); + modify_json(&mut basic_project_json); + + let basic_project_multipart = + request_data::get_public_creation_data_multipart(&basic_project_json, Some(&jar)); + // Add a project- simple, should work. + let req = api.add_public_project( + ProjectCreationRequestData { + slug, + jar: Some(jar), + segment_data: basic_project_multipart, + }, + pat, + ); + async move { + let (project, _) = req.await; + + // Approve, so that the project is searchable + let resp = api + .edit_project( + &project.id.to_string(), + json!({ + "status": "approved" + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + (project.id.0, id) + } + }; + + // Test project 0 + let id = 0; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[4..6]); + json["server_side"] = json!("required"); + json["license_id"] = json!("LGPL-3.0-or-later"); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 1 + let id = 1; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[0..2]); + json["client_side"] = json!("optional"); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 2 + let id = 2; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[0..2]); + json["server_side"] = json!("required"); + json["title"] = json!("Mysterious Project"); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 3 + let id = 3; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[0..3]); + json["server_side"] = json!("required"); + json["initial_versions"][0]["game_versions"] = json!(["1.20.4"]); + json["title"] = json!("Mysterious Project"); + json["license_id"] = json!("LicenseRef-All-Rights-Reserved"); // closed source + }; + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 4 + let id = 4; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[0..3]); + json["client_side"] = json!("optional"); + json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + true, + Box::new(modify_json), + )); + + // Test project 5 + let id = 5; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[5..6]); + json["client_side"] = json!("optional"); + json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); + json["license_id"] = json!("LGPL-3.0-or-later"); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 6 + let id = 6; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[5..6]); + json["client_side"] = json!("optional"); + json["server_side"] = json!("required"); + json["license_id"] = json!("LGPL-3.0-or-later"); + }; + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Box::new(modify_json), + )); + + // Test project 7 (testing the search bug) + // This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version. + // This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project. + let id = 7; + let modify_json = |json: &mut serde_json::Value| { + json["categories"] = json!(DUMMY_CATEGORIES[5..6]); + json["client_side"] = json!("optional"); + json["server_side"] = json!("required"); + json["license_id"] = json!("LGPL-3.0-or-later"); + json["initial_versions"][0]["loaders"] = json!(["forge"]); + json["initial_versions"][0]["game_versions"] = json!(["1.20.2"]); + }; + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Box::new(modify_json), + )); + + // Await all project creation + // Returns a mapping of: + // project id -> test id + let id_conversion: Arc> = Arc::new( + futures::future::join_all(project_creation_futures) + .await + .into_iter() + .collect(), + ); + + // Create a second version for project 7 + let project_7 = api + .get_project_deserialized(&format!("{test_name}-searchable-project-7"), USER_USER_PAT) + .await; + api.add_public_version( + get_public_version_creation_data(project_7.id, "1.0.0", TestFile::build_random_jar()), + USER_USER_PAT, + ) + .await; + + // Pairs of: + // 1. vec of search facets + // 2. expected project ids to be returned by this search + let pairs = vec![ + (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), + (json!([["categories:forge"]]), vec![7]), + ( + json!([["categories:fabric", "categories:forge"]]), + vec![0, 1, 2, 3, 4, 5, 6, 7], + ), + (json!([["categories:fabric"], ["categories:forge"]]), vec![]), + ( + json!([ + ["categories:fabric"], + [&format!("categories:{}", DUMMY_CATEGORIES[0])], + ]), + vec![1, 2, 3, 4], + ), + (json!([["project_types:modpack"]]), vec![4]), + (json!([["client_side:required"]]), vec![0, 2, 3, 7]), + (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), + (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), + (json!([["license:MIT"]]), vec![1, 2, 4]), + (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), + (json!([["versions:1.20.5"]]), vec![4, 5]), + // bug fix + ( + json!([ + // Only the forge one has 1.20.2, so its true that this project 'has' + // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. + ["categories:fabric"], + ["versions:1.20.2"] + ]), + vec![], + ), + // Project type change + // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' + (json!([["categories:mrpack"]]), vec![4]), + ( + json!([["categories:mrpack"], ["categories:fabric"]]), + vec![4], + ), + ( + json!([ + ["categories:mrpack"], + ["categories:fabric"], + ["project_type:modpack"] + ]), + vec![4], + ), + ]; + + // TODO: Untested: + // - downloads (not varied) + // - color (not varied) + // - created_timestamp (not varied) + // - modified_timestamp (not varied) + + // Forcibly reset the search index + let resp = api.reset_search_index().await; + assert_eq!(resp.status(), 204); + + // Test searches + let stream = futures::stream::iter(pairs); + stream + .for_each_concurrent(1, |(facets, mut expected_project_ids)| { + let id_conversion = id_conversion.clone(); + let test_name = test_name.clone(); + async move { + let projects = api + .search_deserialized(Some(&test_name), Some(facets.clone()), USER_USER_PAT) + .await; + let mut found_project_ids: Vec = projects + .hits + .into_iter() + .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) + .collect(); + expected_project_ids.sort(); + found_project_ids.sort(); + assert_eq!(found_project_ids, expected_project_ids); + } + }) + .await; + + // Cleanup test db + test_env.cleanup().await; +} diff --git a/tests/v2/tags.rs b/tests/v2/tags.rs new file mode 100644 index 00000000..037219d3 --- /dev/null +++ b/tests/v2/tags.rs @@ -0,0 +1,74 @@ +use crate::common::environment::TestEnvironment; +use std::collections::HashSet; + +#[actix_rt::test] +async fn get_tags() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let game_versions = api.get_game_versions_deserialized().await; + let loaders = api.get_loaders_deserialized().await; + let side_types = api.get_side_types_deserialized().await; + let categories = api.get_categories_deserialized().await; + + // These tests match dummy data and will need to be updated if the dummy data changes; + let game_version_versions = game_versions + .into_iter() + .map(|x| x.version) + .collect::>(); + assert_eq!( + game_version_versions, + [ + "1.20.1", + "1.20.2", + "1.20.3", + "1.20.4", + "1.20.5", + "Ordering_Negative1", + "Ordering_Positive100" + ] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "mrpack"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let side_type_names = side_types.into_iter().collect::>(); + assert_eq!( + side_type_names, + ["unknown", "required", "optional", "unsupported"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let category_names = categories + .into_iter() + .map(|x| x.name) + .collect::>(); + assert_eq!( + category_names, + [ + "combat", + "economy", + "food", + "optimization", + "decoration", + "mobs", + "magic" + ] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + test_env.cleanup().await; +} diff --git a/tests/v2/version.rs b/tests/v2/version.rs new file mode 100644 index 00000000..dfa215b2 --- /dev/null +++ b/tests/v2/version.rs @@ -0,0 +1,409 @@ +use actix_web::test; +use futures::StreamExt; +use labrinth::models::projects::{ProjectId, VersionId}; +use labrinth::{ + models::{ + ids::base62_impl::parse_base62, + projects::{Loader, VersionStatus, VersionType}, + }, + routes::v2::version_file::FileUpdateData, +}; +use serde_json::json; + +use crate::common::api_v2::request_data::get_public_version_creation_data; +use crate::common::{ + database::{ENEMY_USER_PAT, USER_USER_PAT}, + dummy_data::TestFile, + environment::TestEnvironment, +}; + +#[actix_rt::test] +pub async fn test_patch_version() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + + // // First, we do some patch requests that should fail. + // // Failure because the user is not authorized. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "test 1", + }), + ENEMY_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "scheduled"] { + let resp = api + .edit_version( + alpha_version_id, + json!({ + "status": req, + // requested status it not set here, but in /schedule + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Sucessful request to patch many fields. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "new version name", + "version_number": "1.3.0", + "changelog": "new changelog", + "version_type": "beta", + // // "dependencies": [], TODO: test this + "game_versions": ["1.20.5"], + "loaders": ["forge"], + "featured": false, + // "primary_file": [], TODO: test this + // // "downloads": 0, TODO: moderator exclusive + "status": "draft", + // // "filetypes": ["jar"], TODO: test this + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.name, "new version name"); + assert_eq!(version.version_number, "1.3.0"); + assert_eq!(version.changelog, "new changelog"); + assert_eq!( + version.version_type, + serde_json::from_str::("\"beta\"").unwrap() + ); + assert_eq!(version.game_versions, vec!["1.20.5"]); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); + assert!(!version.featured); + assert_eq!(version.status, VersionStatus::from_string("draft")); + + // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' + // works as expected, as well as only 'loaders' + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": ["1.20.1", "1.20.2", "1.20.4"], + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch + + let resp = api + .edit_version( + alpha_version_id, + json!({ + "loaders": ["fabric"], + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch + assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); + + // Cleanup test db + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn version_updates() { + // Test setup and dummy data + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + let alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + let beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; + + // Quick test, using get version from hash + let version = api + .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT) + .await; + assert_eq!(&version.id.to_string(), alpha_version_id); + + // Get versions from hash + let versions = api + .get_versions_from_hashes_deserialized( + &[alpha_version_hash.as_str(), beta_version_hash.as_str()], + "sha1", + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 2); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); + + // When there is only the one version, there should be no updates + let version = api + .get_update_from_hash_deserialized( + alpha_version_hash, + "sha1", + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(&version.id.to_string(), alpha_version_id); + + let versions = api + .update_files_deserialized( + "sha1", + vec![alpha_version_hash.to_string()], + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + + // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders + let mut update_ids = vec![]; + for (version_number, patch_value) in [ + ( + "0.9.9", + json!({ + "game_versions": ["1.20.1"], + }), + ), + ( + "1.5.0", + json!({ + "game_versions": ["1.20.3"], + "loaders": ["fabric"], + }), + ), + ( + "1.5.1", + json!({ + "game_versions": ["1.20.4"], + "loaders": ["forge"], + "version_type": "beta" + }), + ), + ] + .iter() + { + let version = api + .add_public_version( + get_public_version_creation_data( + ProjectId(parse_base62(alpha_project_id).unwrap()), + version_number, + TestFile::build_random_jar(), + ), + USER_USER_PAT, + ) + .await; + update_ids.push(version.id); + + // Patch using json + api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) + .await; + } + + let check_expected = |game_versions: Option>, + loaders: Option>, + version_types: Option>, + result_id: Option| async move { + let (success, result_id) = match result_id { + Some(id) => (true, id), + None => (false, VersionId(0)), + }; + // get_update_from_hash + let resp = api + .get_update_from_hash( + alpha_version_hash, + "sha1", + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(resp.status(), 200); + let body: serde_json::Value = test::read_body_json(resp).await; + let id = body["id"].as_str().unwrap(); + assert_eq!(id, &result_id.to_string()); + } else { + assert_eq!(resp.status(), 404); + } + + // update_files + let versions = api + .update_files_deserialized( + "sha1", + vec![alpha_version_hash.to_string()], + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + + // update_individual_files + let hashes = vec![FileUpdateData { + hash: alpha_version_hash.to_string(), + loaders, + game_versions, + version_types: version_types.map(|v| { + v.into_iter() + .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) + .collect() + }), + }]; + let versions = api + .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + }; + + let tests = vec![ + check_expected( + Some(vec!["1.20.1".to_string()]), + None, + None, + Some(update_ids[0]), + ), + check_expected( + Some(vec!["1.20.3".to_string()]), + None, + None, + Some(update_ids[1]), + ), + check_expected( + Some(vec!["1.20.4".to_string()]), + None, + None, + Some(update_ids[2]), + ), + // Loader restrictions + check_expected( + None, + Some(vec!["fabric".to_string()]), + None, + Some(update_ids[1]), + ), + check_expected( + None, + Some(vec!["forge".to_string()]), + None, + Some(update_ids[2]), + ), + // Version type restrictions + check_expected( + None, + None, + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + check_expected( + None, + None, + Some(vec!["beta".to_string()]), + Some(update_ids[2]), + ), + // Specific combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + // Impossible combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["beta".to_string()]), + None, + ), + // No restrictions, should do the last one + check_expected(None, None, None, Some(update_ids[2])), + ]; + + // Wait on all tests, 4 at a time + futures::stream::iter(tests) + .buffer_unordered(4) + .collect::>() + .await; + + // We do a couple small tests for get_project_versions_deserialized as well + // TODO: expand this more. + let versions = api + .get_project_versions_deserialized( + alpha_project_id, + None, + None, + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 4); + let versions = api + .get_project_versions_deserialized( + alpha_project_id, + None, + Some(vec!["forge".to_string()]), + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + + // Cleanup test db + test_env.cleanup().await; +} diff --git a/tests/v2_tests.rs b/tests/v2_tests.rs new file mode 100644 index 00000000..839cc303 --- /dev/null +++ b/tests/v2_tests.rs @@ -0,0 +1,16 @@ +// importing common module. +mod common; + +// Not all tests expect exactly the same functionality in v2 and v3. +// For example, though we expect the /GET version to return the corresponding project, +// we may want to do different checks for each. +// (such as checking client_side in v2, but loader fields on v3- which are model-exclusie) + +// Such V2 tests are exported here +mod v2 { + mod project; + mod scopes; + mod search; + mod tags; + mod version; +} diff --git a/tests/version.rs b/tests/version.rs index 2edd400e..665cbf58 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -1,16 +1,18 @@ +use std::collections::HashMap; + use actix_web::test; use common::environment::TestEnvironment; use futures::StreamExt; use labrinth::database::models::version_item::VERSIONS_NAMESPACE; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::projects::{Loader, ProjectId, VersionId, VersionStatus, VersionType}; -use labrinth::routes::v2::version_file::FileUpdateData; +use labrinth::routes::v3::version_file::FileUpdateData; use serde_json::json; +use crate::common::api_v3::request_data::get_public_version_creation_data; use crate::common::database::*; use crate::common::dummy_data::TestFile; -use crate::common::request_data::get_public_version_creation_data; // importing common module. mod common; @@ -19,7 +21,7 @@ mod common; async fn test_get_version() { // Test setup and dummy data let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; @@ -68,11 +70,10 @@ async fn test_get_version() { } #[actix_rt::test] - async fn version_updates() { // Test setup and dummy data let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; @@ -158,11 +159,12 @@ async fn version_updates() { .iter() { let version = api - .add_public_version( + .add_public_version_deserialized( get_public_version_creation_data( ProjectId(parse_base62(alpha_project_id).unwrap()), version_number, TestFile::build_random_jar(), + None::, ), USER_USER_PAT, ) @@ -222,10 +224,21 @@ async fn version_updates() { } // update_individual_files + let mut loader_fields = HashMap::new(); + if let Some(game_versions) = game_versions { + loader_fields.insert( + "game_versions".to_string(), + game_versions + .into_iter() + .map(|v| json!(v)) + .collect::>(), + ); + } + let hashes = vec![FileUpdateData { hash: alpha_version_hash.to_string(), loaders, - game_versions, + loader_fields: Some(loader_fields), version_types: version_types.map(|v| { v.into_iter() .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) @@ -349,7 +362,7 @@ async fn version_updates() { #[actix_rt::test] pub async fn test_patch_version() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; @@ -414,7 +427,6 @@ pub async fn test_patch_version() { version.version_type, serde_json::from_str::("\"beta\"").unwrap() ); - assert_eq!(version.game_versions, vec!["1.20.5"]); assert_eq!(version.loaders, vec![Loader("forge".to_string())]); assert!(!version.featured); assert_eq!(version.status, VersionStatus::from_string("draft")); @@ -435,7 +447,6 @@ pub async fn test_patch_version() { let version = api .get_version_deserialized(alpha_version_id, USER_USER_PAT) .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch let resp = api @@ -452,7 +463,6 @@ pub async fn test_patch_version() { let version = api .get_version_deserialized(alpha_version_id, USER_USER_PAT) .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); // Cleanup test db @@ -462,7 +472,7 @@ pub async fn test_patch_version() { #[actix_rt::test] pub async fn test_project_versions() { let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; + let api = &test_env.v3; let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; let _beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; @@ -498,14 +508,14 @@ async fn can_create_version_with_ordering() { let alpha_project_id = env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); let new_version_id = get_json_val_str( - env.v2 + env.v3 .create_default_version(&alpha_project_id, Some(1), USER_USER_PAT) .await .id, ); let versions = env - .v2 + .v3 .get_versions(vec![new_version_id.clone()], USER_USER_PAT) .await; assert_eq!(versions[0].ordering, Some(1)); @@ -519,13 +529,13 @@ async fn edit_version_ordering_works() { let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let resp = env - .v2 + .v3 .edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT) .await; assert_status(&resp, StatusCode::NO_CONTENT); let versions = env - .v2 + .v3 .get_versions(vec![alpha_version_id.clone()], USER_USER_PAT) .await; assert_eq!(versions[0].ordering, Some(10)); @@ -539,17 +549,17 @@ async fn version_ordering_for_specified_orderings_orders_lower_order_first() { let alpha_project_id = env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v2 + env.v3 .create_default_version(&alpha_project_id, Some(1), USER_USER_PAT) .await .id, ); - env.v2 + env.v3 .edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT) .await; let versions = env - .v2 + .v3 .get_versions( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, @@ -566,14 +576,14 @@ async fn version_ordering_when_unspecified_orders_oldest_first() { let alpha_project_id = &env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v2 + env.v3 .create_default_version(alpha_project_id, None, USER_USER_PAT) .await .id, ); let versions = env - .v2 + .v3 .get_versions( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, @@ -590,17 +600,17 @@ async fn version_ordering_when_specified_orders_specified_before_unspecified() { let alpha_project_id = &env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v2 + env.v3 .create_default_version(alpha_project_id, Some(10000), USER_USER_PAT) .await .id, ); - env.v2 + env.v3 .edit_version_ordering(&alpha_version_id, None, USER_USER_PAT) .await; let versions = env - .v2 + .v3 .get_versions( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, From e06a77af28d760f9d5aa7a889b3ad014e90fd8e8 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 17 Nov 2023 17:58:15 -0800 Subject: [PATCH 3/5] Adds code coverage (#757) * coverage initial push * compiles on PR * adds db env variable * fixed env variables being on the wrong action * added more tests yml code * refresh * tried copying over tests.yml * removed accidental tests * shotgun attempts * generated yml * more tries * shotgun again * small mistakes * repush * repush * Adds env variables to tarp * removes unused actions and tests cfg attribute on main.rs * only will work on push to master * changed to 60% --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- .codecov.yml | 13 ++++++++++ .github/workflows/coverage.yml | 44 ++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ src/main.rs | 1 + 4 files changed, 60 insertions(+) create mode 100644 .codecov.yml create mode 100644 .github/workflows/coverage.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..3b187dd0 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,13 @@ +comment: false + +coverage: + status: + project: + default: + threshold: 60% # make CI green + patch: + default: + threshold: 60% # make CI green + +ignore: # ignore code coverage on following paths + - "**/tests" \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..6e67a3ac --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,44 @@ +name: Coverage-Tarpaulin + +env: + CARGO_TERM_COLOR: always + SQLX_OFFLINE: true + +on: + push: + branches: [ master ] + # Uncomment to allow PRs to trigger the workflow + # pull_request: + # branches: [ master ] +jobs: + citarp: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + # Start Docker Compose + - name: Start Docker Compose + run: docker-compose up -d + + - name: Install cargo tarpaulin + uses: taiki-e/install-action@cargo-tarpaulin + - name: Generate code coverage + run: | + cargo tarpaulin --verbose --all-features --timeout 120 --out xml + env: + BACKBLAZE_BUCKET_ID: ${{ secrets.BACKBLAZE_BUCKET_ID }} + BACKBLAZE_KEY: ${{ secrets.BACKBLAZE_KEY }} + BACKBLAZE_KEY_ID: ${{ secrets.BACKBLAZE_KEY_ID }} + S3_ACCESS_TOKEN: ${{ secrets.S3_ACCESS_TOKEN }} + S3_SECRET: ${{ secrets.S3_SECRET }} + S3_URL: ${{ secrets.S3_URL }} + S3_REGION: ${{ secrets.S3_REGION }} + S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} + SQLX_OFFLINE: true + DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres + + - name: Upload to codecov.io + uses: codecov/codecov-action@v2 + with: + # token: ${{secrets.CODECOV_TOKEN}} # not required for public repos + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 0fcb7d0a..516893c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +codecov.json + # Created by https://www.gitignore.io/api/rust,clion # Edit at https://www.gitignore.io/?templates=rust,clion diff --git a/src/main.rs b/src/main.rs index 4b25580e..7aff4d60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ pub struct Pepper { pub pepper: String, } +#[cfg(not(tarpaulin_include))] #[actix_rt::main] async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok(); From dfba6c7c91d244fde274a6ea6c537433a882f5ca Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Sun, 19 Nov 2023 19:10:13 -0800 Subject: [PATCH 4/5] Compiler improvements (#753) * basic redis add * toml; reverted unnecessary changes * merge issues * increased test connections --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- Cargo.toml | 6 + src/database/models/categories.rs | 8 ++ src/database/models/collection_item.rs | 9 +- src/database/models/flow_item.rs | 6 + src/database/models/image_item.rs | 5 +- src/database/models/loader_fields.rs | 12 +- src/database/models/notification_item.rs | 6 +- src/database/models/organization_item.rs | 18 ++- src/database/models/pat_item.rs | 20 ++- src/database/models/project_item.rs | 22 ++- src/database/models/session_item.rs | 22 ++- src/database/models/team_item.rs | 10 +- src/database/models/user_item.rs | 21 ++- src/database/models/version_item.rs | 23 ++- src/database/redis.rs | 169 ++++++++++++++--------- src/util/mod.rs | 1 + src/util/redis.rs | 18 +++ tests/project.rs | 38 ++--- tests/search.rs | 2 +- tests/v2/project.rs | 19 ++- tests/v2/search.rs | 2 +- tests/version.rs | 10 +- 22 files changed, 307 insertions(+), 140 deletions(-) create mode 100644 src/util/redis.rs diff --git a/Cargo.toml b/Cargo.toml index bb38733c..040ed0c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,3 +109,9 @@ derive-new = "0.5.9" [dev-dependencies] actix-http = "3.4.0" + +[profile.dev] +opt-level = 0 # Minimal optimization, speeds up compilation +lto = false # Disables Link Time Optimization +incremental = true # Enables incremental compilation +codegen-units = 16 # Higher number can improve compile times but reduce runtime performance diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index 95d054f2..6205fab8 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -90,6 +90,8 @@ impl Category { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res: Option> = redis .get_deserialized_from_json(TAGS_NAMESPACE, "category") .await?; @@ -155,6 +157,8 @@ impl DonationPlatform { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res: Option> = redis .get_deserialized_from_json(TAGS_NAMESPACE, "donation_platform") .await?; @@ -209,6 +213,8 @@ impl ReportType { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res: Option> = redis .get_deserialized_from_json(TAGS_NAMESPACE, "report_type") .await?; @@ -257,6 +263,8 @@ impl ProjectType { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res: Option> = redis .get_deserialized_from_json(TAGS_NAMESPACE, "project_type") .await?; diff --git a/src/database/models/collection_item.rs b/src/database/models/collection_item.rs index d000e2ce..4a4f7424 100644 --- a/src/database/models/collection_item.rs +++ b/src/database/models/collection_item.rs @@ -157,6 +157,8 @@ impl Collection { { use futures::TryStreamExt; + let mut redis = redis.connect().await?; + if collection_ids.is_empty() { return Ok(Vec::new()); } @@ -166,7 +168,10 @@ impl Collection { if !collection_ids.is_empty() { let collections = redis - .multi_get::(COLLECTIONS_NAMESPACE, collection_ids.iter().map(|x| x.0)) + .multi_get::( + COLLECTIONS_NAMESPACE, + collection_ids.iter().map(|x| x.0.to_string()), + ) .await?; for collection in collections { @@ -240,6 +245,8 @@ impl Collection { } pub async fn clear_cache(id: CollectionId, redis: &RedisPool) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis.delete(COLLECTIONS_NAMESPACE, id.0).await?; Ok(()) } diff --git a/src/database/models/flow_item.rs b/src/database/models/flow_item.rs index fe81e4a8..22d30895 100644 --- a/src/database/models/flow_item.rs +++ b/src/database/models/flow_item.rs @@ -58,6 +58,8 @@ impl Flow { expires: Duration, redis: &RedisPool, ) -> Result { + let mut redis = redis.connect().await?; + let flow = ChaCha20Rng::from_entropy() .sample_iter(&Alphanumeric) .take(32) @@ -71,6 +73,8 @@ impl Flow { } pub async fn get(id: &str, redis: &RedisPool) -> Result, DatabaseError> { + let mut redis = redis.connect().await?; + redis.get_deserialized_from_json(FLOWS_NAMESPACE, id).await } @@ -91,6 +95,8 @@ impl Flow { } pub async fn remove(id: &str, redis: &RedisPool) -> Result, DatabaseError> { + let mut redis = redis.connect().await?; + redis.delete(FLOWS_NAMESPACE, id).await?; Ok(Some(())) } diff --git a/src/database/models/image_item.rs b/src/database/models/image_item.rs index 34badd65..68477304 100644 --- a/src/database/models/image_item.rs +++ b/src/database/models/image_item.rs @@ -180,6 +180,7 @@ impl Image { { use futures::TryStreamExt; + let mut redis = redis.connect().await?; if image_ids.is_empty() { return Ok(Vec::new()); } @@ -191,7 +192,7 @@ impl Image { if !image_ids.is_empty() { let images = redis - .multi_get::(IMAGES_NAMESPACE, image_ids) + .multi_get::(IMAGES_NAMESPACE, image_ids.iter().map(|x| x.to_string())) .await?; for image in images { if let Some(image) = image.and_then(|x| serde_json::from_str::(&x).ok()) { @@ -246,6 +247,8 @@ impl Image { } pub async fn clear_cache(id: ImageId, redis: &RedisPool) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis.delete(IMAGES_NAMESPACE, id.0).await?; Ok(()) } diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index a4b101c3..63e1d6fd 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -44,6 +44,7 @@ impl Game { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; let cached_games: Option> = redis .get_deserialized_from_json(GAMES_LIST_NAMESPACE, "games") .await?; @@ -95,6 +96,7 @@ impl Loader { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; let cached_id: Option = redis.get_deserialized_from_json(LOADER_ID, name).await?; if let Some(cached_id) = cached_id { return Ok(Some(LoaderId(cached_id))); @@ -124,6 +126,7 @@ impl Loader { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; let cached_loaders: Option> = redis .get_deserialized_from_json(LOADERS_LIST_NAMESPACE, "all") .await?; @@ -318,9 +321,11 @@ impl LoaderField { { type RedisLoaderFieldTuple = (LoaderId, Vec); + let mut redis = redis.connect().await?; + let mut loader_ids = loader_ids.to_vec(); let cached_fields: Vec = redis - .multi_get::(LOADER_FIELDS_NAMESPACE, loader_ids.iter().map(|x| x.0)) + .multi_get::(LOADER_FIELDS_NAMESPACE, loader_ids.iter().map(|x| x.0)) .await? .into_iter() .flatten() @@ -399,6 +404,8 @@ impl LoaderFieldEnum { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let cached_enum = redis .get_deserialized_from_json(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name) .await?; @@ -488,12 +495,13 @@ impl LoaderFieldEnumValue { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; let mut found_enums = Vec::new(); let mut remaining_enums: Vec = loader_field_enum_ids.to_vec(); if !remaining_enums.is_empty() { let enums = redis - .multi_get::( + .multi_get::( LOADER_FIELD_ENUM_VALUES_NAMESPACE, loader_field_enum_ids.iter().map(|x| x.0), ) diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index 2b15a4bd..2bc89fec 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -174,8 +174,10 @@ impl Notification { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { + let mut redis = redis.connect().await?; + let cached_notifications: Option> = redis - .get_deserialized_from_json(USER_NOTIFICATIONS_NAMESPACE, user_id.0) + .get_deserialized_from_json(USER_NOTIFICATIONS_NAMESPACE, &user_id.0.to_string()) .await?; if let Some(notifications) = cached_notifications { @@ -319,6 +321,8 @@ impl Notification { user_ids: impl IntoIterator, redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many( user_ids diff --git a/src/database/models/organization_item.rs b/src/database/models/organization_item.rs index f92622df..137d7ae0 100644 --- a/src/database/models/organization_item.rs +++ b/src/database/models/organization_item.rs @@ -103,6 +103,8 @@ impl Organization { { use futures::stream::TryStreamExt; + let mut redis = redis.connect().await?; + if organization_strings.is_empty() { return Ok(Vec::new()); } @@ -120,11 +122,12 @@ impl Organization { organization_ids.append( &mut redis - .multi_get::( + .multi_get::( ORGANIZATIONS_TITLES_NAMESPACE, organization_strings .iter() - .map(|x| x.to_string().to_lowercase()), + .map(|x| x.to_string().to_lowercase()) + .collect::>(), ) .await? .into_iter() @@ -134,7 +137,10 @@ impl Organization { if !organization_ids.is_empty() { let organizations = redis - .multi_get::(ORGANIZATIONS_NAMESPACE, organization_ids) + .multi_get::( + ORGANIZATIONS_NAMESPACE, + organization_ids.iter().map(|x| x.to_string()), + ) .await?; for organization in organizations { @@ -197,8 +203,8 @@ impl Organization { redis .set( ORGANIZATIONS_TITLES_NAMESPACE, - organization.title.to_lowercase(), - organization.id.0, + &organization.title.to_lowercase(), + &organization.id.0.to_string(), None, ) .await?; @@ -318,6 +324,8 @@ impl Organization { title: Option, redis: &RedisPool, ) -> Result<(), super::DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many([ (ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())), diff --git a/src/database/models/pat_item.rs b/src/database/models/pat_item.rs index fc2432ae..9352d637 100644 --- a/src/database/models/pat_item.rs +++ b/src/database/models/pat_item.rs @@ -89,6 +89,8 @@ impl PersonalAccessToken { { use futures::TryStreamExt; + let mut redis = redis.connect().await?; + if pat_strings.is_empty() { return Ok(Vec::new()); } @@ -106,7 +108,7 @@ impl PersonalAccessToken { pat_ids.append( &mut redis - .multi_get::( + .multi_get::( PATS_TOKENS_NAMESPACE, pat_strings.iter().map(|x| x.to_string()), ) @@ -118,7 +120,7 @@ impl PersonalAccessToken { if !pat_ids.is_empty() { let pats = redis - .multi_get::(PATS_NAMESPACE, pat_ids) + .multi_get::(PATS_NAMESPACE, pat_ids.iter().map(|x| x.to_string())) .await?; for pat in pats { if let Some(pat) = @@ -174,8 +176,8 @@ impl PersonalAccessToken { redis .set( PATS_TOKENS_NAMESPACE, - pat.access_token.clone(), - pat.id.0, + &pat.access_token, + &pat.id.0.to_string(), None, ) .await?; @@ -194,8 +196,10 @@ impl PersonalAccessToken { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res = redis - .get_deserialized_from_json::, _>(PATS_USERS_NAMESPACE, user_id.0) + .get_deserialized_from_json::>(PATS_USERS_NAMESPACE, &user_id.0.to_string()) .await?; if let Some(res) = res { @@ -220,8 +224,8 @@ impl PersonalAccessToken { redis .set( PATS_USERS_NAMESPACE, - user_id.0, - serde_json::to_string(&db_pats)?, + &user_id.0.to_string(), + &serde_json::to_string(&db_pats)?, None, ) .await?; @@ -232,6 +236,8 @@ impl PersonalAccessToken { clear_pats: Vec<(Option, Option, Option)>, redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + if clear_pats.is_empty() { return Ok(()); } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 61dd2464..6be0f01b 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -513,6 +513,8 @@ impl Project { return Ok(Vec::new()); } + let mut redis = redis.connect().await?; + let mut found_projects = Vec::new(); let mut remaining_strings = project_strings .iter() @@ -526,7 +528,7 @@ impl Project { project_ids.append( &mut redis - .multi_get::( + .multi_get::( PROJECTS_SLUGS_NAMESPACE, project_strings.iter().map(|x| x.to_string().to_lowercase()), ) @@ -537,7 +539,10 @@ impl Project { ); if !project_ids.is_empty() { let projects = redis - .multi_get::(PROJECTS_NAMESPACE, project_ids) + .multi_get::( + PROJECTS_NAMESPACE, + project_ids.iter().map(|x| x.to_string()), + ) .await?; for project in projects { if let Some(project) = @@ -686,8 +691,8 @@ impl Project { redis .set( PROJECTS_SLUGS_NAMESPACE, - slug.to_lowercase(), - project.inner.id.0, + &slug.to_lowercase(), + &project.inner.id.0.to_string(), None, ) .await?; @@ -709,8 +714,13 @@ impl Project { { type Dependencies = Vec<(Option, Option, Option)>; + let mut redis = redis.connect().await?; + let dependencies = redis - .get_deserialized_from_json::(PROJECTS_DEPENDENCIES_NAMESPACE, id.0) + .get_deserialized_from_json::( + PROJECTS_DEPENDENCIES_NAMESPACE, + &id.0.to_string(), + ) .await?; if let Some(dependencies) = dependencies { return Ok(dependencies); @@ -755,6 +765,8 @@ impl Project { clear_dependencies: Option, redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many([ (PROJECTS_NAMESPACE, Some(id.0.to_string())), diff --git a/src/database/models/session_item.rs b/src/database/models/session_item.rs index ff9a874e..f27af5bb 100644 --- a/src/database/models/session_item.rs +++ b/src/database/models/session_item.rs @@ -130,6 +130,8 @@ impl Session { { use futures::TryStreamExt; + let mut redis = redis.connect().await?; + if session_strings.is_empty() { return Ok(Vec::new()); } @@ -147,7 +149,7 @@ impl Session { session_ids.append( &mut redis - .multi_get::( + .multi_get::( SESSIONS_IDS_NAMESPACE, session_strings.iter().map(|x| x.to_string()), ) @@ -159,7 +161,10 @@ impl Session { if !session_ids.is_empty() { let sessions = redis - .multi_get::(SESSIONS_NAMESPACE, session_ids) + .multi_get::( + SESSIONS_NAMESPACE, + session_ids.iter().map(|x| x.to_string()), + ) .await?; for session in sessions { if let Some(session) = @@ -218,8 +223,8 @@ impl Session { redis .set( SESSIONS_IDS_NAMESPACE, - session.session.clone(), - session.id.0, + &session.session, + &session.id.0.to_string(), None, ) .await?; @@ -238,8 +243,13 @@ impl Session { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let mut redis = redis.connect().await?; + let res = redis - .get_deserialized_from_json::, _>(SESSIONS_USERS_NAMESPACE, user_id.0) + .get_deserialized_from_json::>( + SESSIONS_USERS_NAMESPACE, + &user_id.0.to_string(), + ) .await?; if let Some(res) = res { @@ -272,6 +282,8 @@ impl Session { clear_sessions: Vec<(Option, Option, Option)>, redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + if clear_sessions.is_empty() { return Ok(()); } diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index a513aefe..a0a92f70 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -197,18 +197,23 @@ impl TeamMember { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { + use futures::stream::TryStreamExt; + if team_ids.is_empty() { return Ok(Vec::new()); } - use futures::stream::TryStreamExt; + let mut redis = redis.connect().await?; let mut team_ids_parsed: Vec = team_ids.iter().map(|x| x.0).collect(); let mut found_teams = Vec::new(); let teams = redis - .multi_get::(TEAMS_NAMESPACE, team_ids_parsed.clone()) + .multi_get::( + TEAMS_NAMESPACE, + team_ids_parsed.iter().map(|x| x.to_string()), + ) .await?; for team_raw in teams { @@ -271,6 +276,7 @@ impl TeamMember { } pub async fn clear_cache(id: TeamId, redis: &RedisPool) -> Result<(), super::DatabaseError> { + let mut redis = redis.connect().await?; redis.delete(TEAMS_NAMESPACE, id.0).await?; Ok(()) } diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 5ab27abe..8230ff58 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -134,6 +134,8 @@ impl User { { use futures::TryStreamExt; + let mut redis = redis.connect().await?; + if users_strings.is_empty() { return Ok(Vec::new()); } @@ -151,7 +153,7 @@ impl User { user_ids.append( &mut redis - .multi_get::( + .multi_get::( USER_USERNAMES_NAMESPACE, users_strings.iter().map(|x| x.to_string().to_lowercase()), ) @@ -163,7 +165,7 @@ impl User { if !user_ids.is_empty() { let users = redis - .multi_get::(USERS_NAMESPACE, user_ids) + .multi_get::(USERS_NAMESPACE, user_ids.iter().map(|x| x.to_string())) .await?; for user in users { if let Some(user) = user.and_then(|x| serde_json::from_str::(&x).ok()) { @@ -239,8 +241,8 @@ impl User { redis .set( USER_USERNAMES_NAMESPACE, - user.username.to_lowercase(), - user.id.0, + &user.username.to_lowercase(), + &user.id.0.to_string(), None, ) .await?; @@ -278,8 +280,13 @@ impl User { { use futures::stream::TryStreamExt; + let mut redis = redis.connect().await?; + let cached_projects = redis - .get_deserialized_from_json::, _>(USERS_PROJECTS_NAMESPACE, user_id.0) + .get_deserialized_from_json::>( + USERS_PROJECTS_NAMESPACE, + &user_id.0.to_string(), + ) .await?; if let Some(projects) = cached_projects { @@ -384,6 +391,8 @@ impl User { user_ids: &[(UserId, Option)], redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many(user_ids.iter().flat_map(|(id, username)| { [ @@ -402,6 +411,8 @@ impl User { user_ids: &[UserId], redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many( user_ids diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index b01106e0..3e3fc3ec 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -492,18 +492,27 @@ impl Version { where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + use futures::stream::TryStreamExt; + if version_ids.is_empty() { return Ok(Vec::new()); } - use futures::stream::TryStreamExt; + let mut redis = redis.connect().await?; let mut version_ids_parsed: Vec = version_ids.iter().map(|x| x.0).collect(); let mut found_versions = Vec::new(); let versions = redis - .multi_get::(VERSIONS_NAMESPACE, version_ids_parsed.clone()) + .multi_get::( + VERSIONS_NAMESPACE, + version_ids_parsed + .clone() + .iter() + .map(|x| x.to_string()) + .collect::>(), + ) .await?; for version in versions { @@ -721,18 +730,20 @@ impl Version { where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { + use futures::stream::TryStreamExt; + + let mut redis = redis.connect().await?; + if hashes.is_empty() { return Ok(Vec::new()); } - use futures::stream::TryStreamExt; - let mut file_ids_parsed = hashes.to_vec(); let mut found_files = Vec::new(); let files = redis - .multi_get::( + .multi_get::( VERSION_FILES_NAMESPACE, file_ids_parsed .iter() @@ -829,6 +840,8 @@ impl Version { version: &QueryVersion, redis: &RedisPool, ) -> Result<(), DatabaseError> { + let mut redis = redis.connect().await?; + redis .delete_many( iter::once((VERSIONS_NAMESPACE, Some(version.inner.id.0.to_string()))).chain( diff --git a/src/database/redis.rs b/src/database/redis.rs index 2a517264..f121e3e9 100644 --- a/src/database/redis.rs +++ b/src/database/redis.rs @@ -1,6 +1,7 @@ use super::models::DatabaseError; use deadpool_redis::{Config, Runtime}; -use redis::{cmd, FromRedisValue, ToRedisArgs}; +use itertools::Itertools; +use redis::{cmd, Cmd}; use std::fmt::Display; const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes @@ -11,6 +12,11 @@ pub struct RedisPool { meta_namespace: String, } +pub struct RedisConnection { + pub connection: deadpool_redis::Connection, + meta_namespace: String, +} + impl RedisPool { // initiate a new redis pool // testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests) @@ -35,32 +41,39 @@ impl RedisPool { } } - pub async fn set( - &self, + pub async fn connect(&self) -> Result { + Ok(RedisConnection { + connection: self.pool.get().await?, + meta_namespace: self.meta_namespace.clone(), + }) + } +} + +impl RedisConnection { + pub async fn set( + &mut self, namespace: &str, - id: T1, - data: T2, + id: &str, + data: &str, expiry: Option, - ) -> Result<(), DatabaseError> - where - T1: Display, - T2: ToRedisArgs, - { - let mut redis_connection = self.pool.get().await?; - - cmd("SET") - .arg(format!("{}_{}:{}", self.meta_namespace, namespace, id)) - .arg(data) - .arg("EX") - .arg(expiry.unwrap_or(DEFAULT_EXPIRY)) - .query_async::<_, ()>(&mut redis_connection) - .await?; - + ) -> Result<(), DatabaseError> { + let mut cmd = cmd("SET"); + redis_args( + &mut cmd, + vec![ + format!("{}_{}:{}", self.meta_namespace, namespace, id), + data.to_string(), + "EX".to_string(), + expiry.unwrap_or(DEFAULT_EXPIRY).to_string(), + ] + .as_slice(), + ); + redis_execute(&mut cmd, &mut self.connection).await?; Ok(()) } pub async fn set_serialized_to_json( - &self, + &mut self, namespace: &str, id: Id, data: D, @@ -70,92 +83,116 @@ impl RedisPool { Id: Display, D: serde::Serialize, { - self.set(namespace, id, serde_json::to_string(&data)?, expiry) - .await + self.set( + namespace, + &id.to_string(), + &serde_json::to_string(&data)?, + expiry, + ) + .await } - pub async fn get(&self, namespace: &str, id: Id) -> Result, DatabaseError> - where - Id: Display, - R: FromRedisValue, - { - let mut redis_connection = self.pool.get().await?; - - let res = cmd("GET") - .arg(format!("{}_{}:{}", self.meta_namespace, namespace, id)) - .query_async::<_, Option>(&mut redis_connection) - .await?; + pub async fn get( + &mut self, + namespace: &str, + id: &str, + ) -> Result, DatabaseError> { + let mut cmd = cmd("GET"); + redis_args( + &mut cmd, + vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + ); + let res = redis_execute(&mut cmd, &mut self.connection).await?; Ok(res) } - pub async fn get_deserialized_from_json( - &self, + pub async fn get_deserialized_from_json( + &mut self, namespace: &str, - id: Id, + id: &str, ) -> Result, DatabaseError> where - Id: Display, R: for<'a> serde::Deserialize<'a>, { Ok(self - .get::(namespace, id) + .get(namespace, id) .await? .and_then(|x| serde_json::from_str(&x).ok())) } - pub async fn multi_get( - &self, + pub async fn multi_get( + &mut self, namespace: &str, - ids: impl IntoIterator, + ids: impl IntoIterator, ) -> Result>, DatabaseError> where - T1: Display, - R: FromRedisValue, + R: for<'a> serde::Deserialize<'a>, { - let mut redis_connection = self.pool.get().await?; - let res = cmd("MGET") - .arg( - ids.into_iter() - .map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x)) - .collect::>(), - ) - .query_async::<_, Vec>>(&mut redis_connection) - .await?; - Ok(res) + let mut cmd = cmd("MGET"); + + redis_args( + &mut cmd, + &ids.into_iter() + .map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x)) + .collect_vec(), + ); + let res: Vec> = redis_execute(&mut cmd, &mut self.connection).await?; + Ok(res + .into_iter() + .map(|x| x.and_then(|x| serde_json::from_str(&x).ok())) + .collect()) } - pub async fn delete(&self, namespace: &str, id: T1) -> Result<(), DatabaseError> + pub async fn delete(&mut self, namespace: &str, id: T1) -> Result<(), DatabaseError> where T1: Display, { - let mut redis_connection = self.pool.get().await?; - - cmd("DEL") - .arg(format!("{}_{}:{}", self.meta_namespace, namespace, id)) - .query_async::<_, ()>(&mut redis_connection) - .await?; - + let mut cmd = cmd("DEL"); + redis_args( + &mut cmd, + vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + ); + redis_execute(&mut cmd, &mut self.connection).await?; Ok(()) } pub async fn delete_many( - &self, + &mut self, iter: impl IntoIterator)>, ) -> Result<(), DatabaseError> { let mut cmd = cmd("DEL"); let mut any = false; for (namespace, id) in iter { if let Some(id) = id { - cmd.arg(format!("{}_{}:{}", self.meta_namespace, namespace, id)); + redis_args( + &mut cmd, + [format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(), + ); any = true; } } if any { - let mut redis_connection = self.pool.get().await?; - cmd.query_async::<_, ()>(&mut redis_connection).await?; + redis_execute(&mut cmd, &mut self.connection).await?; } Ok(()) } } + +pub fn redis_args(cmd: &mut Cmd, args: &[String]) { + for arg in args { + cmd.arg(arg); + } +} + +pub async fn redis_execute( + cmd: &mut Cmd, + redis: &mut deadpool_redis::Connection, +) -> Result +where + T: redis::FromRedisValue, +{ + let res = cmd.query_async::<_, T>(redis).await?; + Ok(res) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 5729d570..03512d3e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -7,6 +7,7 @@ pub mod env; pub mod ext; pub mod guards; pub mod img; +pub mod redis; pub mod routes; pub mod validate; pub mod webhook; diff --git a/src/util/redis.rs b/src/util/redis.rs new file mode 100644 index 00000000..b5d33219 --- /dev/null +++ b/src/util/redis.rs @@ -0,0 +1,18 @@ +use redis::Cmd; + +pub fn redis_args(cmd: &mut Cmd, args: &[String]) { + for arg in args { + cmd.arg(arg); + } +} + +pub async fn redis_execute( + cmd: &mut Cmd, + redis: &mut deadpool_redis::Connection, +) -> Result +where + T: redis::FromRedisValue, +{ + let res = cmd.query_async::<_, T>(redis).await?; + Ok(res) +} diff --git a/tests/project.rs b/tests/project.rs index 138acac5..40c9cd30 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -40,20 +40,21 @@ async fn test_get_project() { assert_eq!(versions[0], json!(alpha_version_id)); // Confirm that the request was cached + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); assert_eq!( - test_env - .db - .redis_pool - .get::(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug) + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug) .await - .unwrap(), + .unwrap() + .and_then(|x| x.parse::().ok()), Some(parse_base62(alpha_project_id).unwrap() as i64) ); - let cached_project = test_env - .db - .redis_pool - .get::(PROJECTS_NAMESPACE, parse_base62(alpha_project_id).unwrap()) + let cached_project = redis_pool + .get( + PROJECTS_NAMESPACE, + &parse_base62(alpha_project_id).unwrap().to_string(), + ) .await .unwrap() .unwrap(); @@ -249,22 +250,21 @@ async fn test_add_remove_project() { assert_eq!(resp.status(), 204); // Confirm that the project is gone from the cache + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); assert_eq!( - test_env - .db - .redis_pool - .get::(PROJECTS_SLUGS_NAMESPACE, "demo") + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, "demo") .await - .unwrap(), + .unwrap() + .and_then(|x| x.parse::().ok()), None ); assert_eq!( - test_env - .db - .redis_pool - .get::(PROJECTS_SLUGS_NAMESPACE, id) + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, &id) .await - .unwrap(), + .unwrap() + .and_then(|x| x.parse::().ok()), None ); diff --git a/tests/search.rs b/tests/search.rs index 36483547..120aedd6 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -20,7 +20,7 @@ mod common; #[actix_rt::test] async fn search_projects() { // Test setup and dummy data - let test_env = TestEnvironment::build(Some(8)).await; + let test_env = TestEnvironment::build(Some(10)).await; let api = &test_env.v3; let test_name = test_env.db.database_name.clone(); diff --git a/tests/v2/project.rs b/tests/v2/project.rs index 609b8481..7e56d3a6 100644 --- a/tests/v2/project.rs +++ b/tests/v2/project.rs @@ -221,22 +221,21 @@ async fn test_add_remove_project() { assert_eq!(resp.status(), 204); // Confirm that the project is gone from the cache + let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); assert_eq!( - test_env - .db - .redis_pool - .get::(PROJECTS_SLUGS_NAMESPACE, "demo") + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, "demo") .await - .unwrap(), + .unwrap() + .map(|x| x.parse::().unwrap()), None ); assert_eq!( - test_env - .db - .redis_pool - .get::(PROJECTS_SLUGS_NAMESPACE, id) + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, &id) .await - .unwrap(), + .unwrap() + .map(|x| x.parse::().unwrap()), None ); diff --git a/tests/v2/search.rs b/tests/v2/search.rs index fbe39ca6..1e3ccbdf 100644 --- a/tests/v2/search.rs +++ b/tests/v2/search.rs @@ -17,7 +17,7 @@ async fn search_projects() { // It should drastically simplify this function // Test setup and dummy data - let test_env = TestEnvironment::build(Some(8)).await; + let test_env = TestEnvironment::build(Some(10)).await; let api = &test_env.v2; let test_name = test_env.db.database_name.clone(); diff --git a/tests/version.rs b/tests/version.rs index 665cbf58..57c5c710 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -33,10 +33,12 @@ async fn test_get_version() { assert_eq!(&version.project_id.to_string(), alpha_project_id); assert_eq!(&version.id.to_string(), alpha_version_id); - let cached_project = test_env - .db - .redis_pool - .get::(VERSIONS_NAMESPACE, parse_base62(alpha_version_id).unwrap()) + let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); + let cached_project = redis_conn + .get( + VERSIONS_NAMESPACE, + &parse_base62(alpha_version_id).unwrap().to_string(), + ) .await .unwrap() .unwrap(); From 79e634316dba02fd6da06fa39fdcb615cdc355ed Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Tue, 21 Nov 2023 09:02:07 -0800 Subject: [PATCH 5/5] Analytics permissions (#761) * adds test; permissions fix * clippy --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- src/routes/v3/analytics_get.rs | 161 +++++++++++++++++++++++++++++---- tests/analytics.rs | 96 +++++++++++++++++++- tests/common/api_v3/project.rs | 26 ++++-- tests/common/permissions.rs | 43 +++++++++ 4 files changed, 301 insertions(+), 25 deletions(-) diff --git a/src/routes/v3/analytics_get.rs b/src/routes/v3/analytics_get.rs index dc31c69c..ee12e02e 100644 --- a/src/routes/v3/analytics_get.rs +++ b/src/routes/v3/analytics_get.rs @@ -1,8 +1,10 @@ use super::ApiError; +use crate::database; use crate::database::redis::RedisPool; +use crate::models::teams::ProjectPermissions; use crate::{ - auth::{filter_authorized_projects, filter_authorized_versions, get_user_from_headers}, - database::models::{project_item, user_item, version_item}, + auth::get_user_from_headers, + database::models::user_item, models::{ ids::{ base62_impl::{parse_base62, to_base62}, @@ -351,6 +353,7 @@ pub async fn revenue_get( .try_into() .map_err(|_| ApiError::InvalidInput("Invalid resolution_minutes".to_string()))?; // Get the revenue data + let project_ids = project_ids.unwrap_or_default(); let payouts_values = sqlx::query!( " SELECT mod_id, SUM(amount) amount_sum, DATE_BIN($4::interval, created, TIMESTAMP '2001-01-01') AS interval_start @@ -358,7 +361,7 @@ pub async fn revenue_get( WHERE mod_id = ANY($1) AND created BETWEEN $2 AND $3 GROUP by mod_id, interval_start ORDER BY interval_start ", - &project_ids.unwrap_or_default().into_iter().map(|x| x.0 as i64).collect::>(), + &project_ids.iter().map(|x| x.0 as i64).collect::>(), start_date, end_date, duration, @@ -366,7 +369,10 @@ pub async fn revenue_get( .fetch_all(&**pool) .await?; - let mut hm = HashMap::new(); + let mut hm: HashMap<_, _> = project_ids + .into_iter() + .map(|x| (x.to_string(), HashMap::new())) + .collect::>(); for value in payouts_values { if let Some(mod_id) = value.mod_id { if let Some(amount) = value.amount_sum { @@ -559,7 +565,7 @@ async fn filter_allowed_ids( )); } - // If no project_ids or version_ids are provided, we default to all projects the user has access to + // If no project_ids or version_ids are provided, we default to all projects the user has *public* access to if project_ids.is_none() && version_ids.is_none() { project_ids = Some( user_item::User::get_projects(user.id.into(), &***pool, redis) @@ -572,35 +578,154 @@ async fn filter_allowed_ids( // Convert String list to list of ProjectIds or VersionIds // - Filter out unauthorized projects/versions + let project_ids = if let Some(project_strings) = project_ids { + let projects_data = + database::models::Project::get_many(&project_strings, &***pool, redis).await?; - let project_ids = if let Some(project_ids) = project_ids { - // Submitted project_ids are filtered by the user's permissions - let ids = project_ids + let team_ids = projects_data .iter() - .map(|id| Ok(ProjectId(parse_base62(id)?).into())) - .collect::, ApiError>>()?; - let projects = project_item::Project::get_many_ids(&ids, &***pool, redis).await?; - let ids: Vec = filter_authorized_projects(projects, &Some(user.clone()), pool) - .await? + .map(|x| x.inner.team_id) + .collect::>(); + let team_members = + database::models::TeamMember::get_from_team_full_many(&team_ids, &***pool, redis) + .await?; + + let organization_ids = projects_data + .iter() + .filter_map(|x| x.inner.organization_id) + .collect::>(); + let organizations = + database::models::Organization::get_many_ids(&organization_ids, &***pool, redis) + .await?; + + let organization_team_ids = organizations + .iter() + .map(|x| x.team_id) + .collect::>(); + let organization_team_members = database::models::TeamMember::get_from_team_full_many( + &organization_team_ids, + &***pool, + redis, + ) + .await?; + + let ids = projects_data .into_iter() - .map(|x| x.id) + .filter(|project| { + let team_member = team_members + .iter() + .find(|x| x.team_id == project.inner.team_id && x.user_id == user.id.into()); + + let organization = project + .inner + .organization_id + .and_then(|oid| organizations.iter().find(|x| x.id == oid)); + + let organization_team_member = if let Some(organization) = organization { + organization_team_members + .iter() + .find(|x| x.team_id == organization.team_id && x.user_id == user.id.into()) + } else { + None + }; + + let permissions = ProjectPermissions::get_permissions_by_role( + &user.role, + &team_member.cloned(), + &organization_team_member.cloned(), + ) + .unwrap_or_default(); + + permissions.contains(ProjectPermissions::VIEW_ANALYTICS) + }) + .map(|x| x.inner.id.into()) .collect::>(); + Some(ids) } else { None }; + let version_ids = if let Some(version_ids) = version_ids { // Submitted version_ids are filtered by the user's permissions let ids = version_ids .iter() .map(|id| Ok(VersionId(parse_base62(id)?).into())) .collect::, ApiError>>()?; - let versions = version_item::Version::get_many(&ids, &***pool, redis).await?; - let ids: Vec = filter_authorized_versions(versions, &Some(user), pool) - .await? + let versions_data = database::models::Version::get_many(&ids, &***pool, redis).await?; + let project_ids = versions_data + .iter() + .map(|x| x.inner.project_id) + .collect::>(); + + let projects_data = + database::models::Project::get_many_ids(&project_ids, &***pool, redis).await?; + + let team_ids = projects_data + .iter() + .map(|x| x.inner.team_id) + .collect::>(); + let team_members = + database::models::TeamMember::get_from_team_full_many(&team_ids, &***pool, redis) + .await?; + + let organization_ids = projects_data + .iter() + .filter_map(|x| x.inner.organization_id) + .collect::>(); + let organizations = + database::models::Organization::get_many_ids(&organization_ids, &***pool, redis) + .await?; + + let organization_team_ids = organizations + .iter() + .map(|x| x.team_id) + .collect::>(); + let organization_team_members = database::models::TeamMember::get_from_team_full_many( + &organization_team_ids, + &***pool, + redis, + ) + .await?; + + let ids = projects_data + .into_iter() + .filter(|project| { + let team_member = team_members + .iter() + .find(|x| x.team_id == project.inner.team_id && x.user_id == user.id.into()); + + let organization = project + .inner + .organization_id + .and_then(|oid| organizations.iter().find(|x| x.id == oid)); + + let organization_team_member = if let Some(organization) = organization { + organization_team_members + .iter() + .find(|x| x.team_id == organization.team_id && x.user_id == user.id.into()) + } else { + None + }; + + let permissions = ProjectPermissions::get_permissions_by_role( + &user.role, + &team_member.cloned(), + &organization_team_member.cloned(), + ) + .unwrap_or_default(); + + permissions.contains(ProjectPermissions::VIEW_ANALYTICS) + }) + .map(|x| x.inner.id) + .collect::>(); + + let ids = versions_data .into_iter() - .map(|x| x.id) + .filter(|version| ids.contains(&version.inner.project_id)) + .map(|x| x.inner.id.into()) .collect::>(); + Some(ids) } else { None diff --git a/tests/analytics.rs b/tests/analytics.rs index bc3d80d4..c1f7806d 100644 --- a/tests/analytics.rs +++ b/tests/analytics.rs @@ -1,8 +1,11 @@ +use actix_web::test; use chrono::{DateTime, Duration, Utc}; -use common::database::*; use common::environment::TestEnvironment; +use common::permissions::PermissionsTest; +use common::{database::*, permissions::PermissionsTestContext}; use itertools::Itertools; use labrinth::models::ids::base62_impl::parse_base62; +use labrinth::models::teams::ProjectPermissions; use rust_decimal::{prelude::ToPrimitive, Decimal}; mod common; @@ -70,6 +73,7 @@ pub async fn analytics_revenue() { let analytics = api .get_analytics_revenue_deserialized( vec![&alpha_project_id], + false, None, None, None, @@ -99,6 +103,7 @@ pub async fn analytics_revenue() { let analytics = api .get_analytics_revenue_deserialized( vec![&alpha_project_id], + false, Some(Utc::now() - Duration::days(801)), None, None, @@ -133,3 +138,92 @@ fn to_f64_rounded_up(d: Decimal) -> f64 { fn to_f64_vec_rounded_up(d: Vec) -> Vec { d.into_iter().map(to_f64_rounded_up).collect_vec() } + +#[actix_rt::test] +pub async fn permissions_analytics_revenue() { + let test_env = TestEnvironment::build(None).await; + + let alpha_project_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + let alpha_version_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); + let alpha_team_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + + let view_analytics = ProjectPermissions::VIEW_ANALYTICS; + + // first, do check with a project + let req_gen = |ctx: &PermissionsTestContext| { + let projects_string = serde_json::to_string(&vec![ctx.project_id]).unwrap(); + let projects_string = urlencoding::encode(&projects_string); + test::TestRequest::get().uri(&format!( + "/v3/analytics/revenue?project_ids={projects_string}&resolution_minutes=5", + )) + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_200_json_checks( + // On failure, should have 0 projects returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 project returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 1); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); + + // Now with a version + // Need to use alpha + let req_gen = |_: &PermissionsTestContext| { + let versions_string = serde_json::to_string(&vec![alpha_version_id.clone()]).unwrap(); + let versions_string = urlencoding::encode(&versions_string); + test::TestRequest::get().uri(&format!( + "/v3/analytics/revenue?version_ids={versions_string}&resolution_minutes=5", + )) + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_existing_project(&alpha_project_id, &alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_200_json_checks( + // On failure, should have 0 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 1); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); + + // Cleanup test db + test_env.cleanup().await; +} diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index b4365d9c..2ffc1d7a 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -204,13 +204,21 @@ impl ApiV3 { pub async fn get_analytics_revenue( &self, id_or_slugs: Vec<&str>, + ids_are_version_ids: bool, start_date: Option>, end_date: Option>, resolution_minutes: Option, pat: &str, ) -> ServiceResponse { - let projects_string = serde_json::to_string(&id_or_slugs).unwrap(); - let projects_string = urlencoding::encode(&projects_string); + let pv_string = if ids_are_version_ids { + let version_string: String = serde_json::to_string(&id_or_slugs).unwrap(); + let version_string = urlencoding::encode(&version_string); + format!("version_ids={}", version_string) + } else { + let projects_string: String = serde_json::to_string(&id_or_slugs).unwrap(); + let projects_string = urlencoding::encode(&projects_string); + format!("project_ids={}", projects_string) + }; let mut extra_args = String::new(); if let Some(start_date) = start_date { @@ -230,9 +238,7 @@ impl ApiV3 { } let req = test::TestRequest::get() - .uri(&format!( - "/v3/analytics/revenue?{projects_string}{extra_args}", - )) + .uri(&format!("/v3/analytics/revenue?{pv_string}{extra_args}",)) .append_header(("Authorization", pat)) .to_request(); @@ -242,13 +248,21 @@ impl ApiV3 { pub async fn get_analytics_revenue_deserialized( &self, id_or_slugs: Vec<&str>, + ids_are_version_ids: bool, start_date: Option>, end_date: Option>, resolution_minutes: Option, pat: &str, ) -> HashMap> { let resp = self - .get_analytics_revenue(id_or_slugs, start_date, end_date, resolution_minutes, pat) + .get_analytics_revenue( + id_or_slugs, + ids_are_version_ids, + start_date, + end_date, + resolution_minutes, + pat, + ) .await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs index 1bb2e20a..c960b72f 100644 --- a/tests/common/permissions.rs +++ b/tests/common/permissions.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +use actix_http::StatusCode; use actix_web::test::{self, TestRequest}; use itertools::Itertools; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; @@ -45,6 +46,12 @@ pub struct PermissionsTest<'a> { // The codes that is allow to be returned if the scope is not present. // (for instance, we might expect a 401, but not a 400) allowed_failure_codes: Vec, + + // Closures that check the JSON body of the response for failure and success cases. + // These are used to perform more complex tests than just checking the status code. + // (eg: checking that the response contains the correct data) + failure_json_check: Option>, + success_json_check: Option>, } pub struct PermissionsTestContext<'a> { @@ -71,6 +78,8 @@ impl<'a> PermissionsTest<'a> { project_team_id: None, organization_team_id: None, allowed_failure_codes: vec![401, 404], + failure_json_check: None, + success_json_check: None, } } @@ -87,6 +96,20 @@ impl<'a> PermissionsTest<'a> { self } + // Set check closures for the JSON body of the response + // These are used to perform more complex tests than just checking the status code. + // If not set, no checks will be performed (and the status code is the only check). + // This is useful if, say, both expected status codes are 200. + pub fn with_200_json_checks( + mut self, + failure_json_check: impl Fn(&serde_json::Value) + Send + 'static, + success_json_check: impl Fn(&serde_json::Value) + Send + 'static, + ) -> Self { + self.failure_json_check = Some(Box::new(failure_json_check)); + self.success_json_check = Some(Box::new(success_json_check)); + self + } + // Set the user ID to use // (eg: a moderator, or friend) // remove_user: Whether or not the user ID should be removed from the project/organization team after the test @@ -181,6 +204,11 @@ impl<'a> PermissionsTest<'a> { resp.status().as_u16() )); } + if resp.status() == StatusCode::OK { + if let Some(failure_json_check) = &self.failure_json_check { + failure_json_check(&test::read_body_json(resp).await); + } + } // Failure test- logged in on a non-team user let request = req_gen(&PermissionsTestContext { @@ -202,6 +230,11 @@ impl<'a> PermissionsTest<'a> { resp.status().as_u16() )); } + if resp.status() == StatusCode::OK { + if let Some(failure_json_check) = &self.failure_json_check { + failure_json_check(&test::read_body_json(resp).await); + } + } // Failure test- logged in with EVERY non-relevant permission let request = req_gen(&PermissionsTestContext { @@ -223,6 +256,11 @@ impl<'a> PermissionsTest<'a> { resp.status().as_u16() )); } + if resp.status() == StatusCode::OK { + if let Some(failure_json_check) = &self.failure_json_check { + failure_json_check(&test::read_body_json(resp).await); + } + } // Patch user's permissions to success permissions modify_user_team_permissions( @@ -250,6 +288,11 @@ impl<'a> PermissionsTest<'a> { resp.status().as_u16() )); } + if resp.status() == StatusCode::OK { + if let Some(success_json_check) = &self.success_json_check { + success_json_check(&test::read_body_json(resp).await); + } + } // If the remove_user flag is set, remove the user from the project // Relevant for existing projects/users