diff --git a/.sqlx/query-1af33ce1ecbf8d0ab2dcc6de7d433ca05a82acc32dd447ff51487e0039706fec.json b/.sqlx/query-1af33ce1ecbf8d0ab2dcc6de7d433ca05a82acc32dd447ff51487e0039706fec.json new file mode 100644 index 00000000..fdfa60d6 --- /dev/null +++ b/.sqlx/query-1af33ce1ecbf8d0ab2dcc6de7d433ca05a82acc32dd447ff51487e0039706fec.json @@ -0,0 +1,36 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT mod_id, v.id as id, date_published\n FROM mods m\n INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3)\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "date_published", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray", + "TextArray" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "1af33ce1ecbf8d0ab2dcc6de7d433ca05a82acc32dd447ff51487e0039706fec" +} diff --git a/.sqlx/query-2140809b7b65c44c7de96ce89ca52a1808e134756baf6d847600668b7e0bbc95.json b/.sqlx/query-2140809b7b65c44c7de96ce89ca52a1808e134756baf6d847600668b7e0bbc95.json new file mode 100644 index 00000000..9b62665f --- /dev/null +++ b/.sqlx/query-2140809b7b65c44c7de96ce89ca52a1808e134756baf6d847600668b7e0bbc95.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT id, enum_id, value, ordering, created, metadata\n FROM loader_field_enum_values lfev\n WHERE id = ANY($1) \n ORDER BY enum_id, ordering, created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "enum_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "value", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "ordering", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "metadata", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int4Array" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + true + ] + }, + "hash": "2140809b7b65c44c7de96ce89ca52a1808e134756baf6d847600668b7e0bbc95" +} diff --git a/.sqlx/query-2390acbe75f9956e8e16c29faa90aa2fb6b3e11a417302b62fc4a6b4a1785f75.json b/.sqlx/query-2390acbe75f9956e8e16c29faa90aa2fb6b3e11a417302b62fc4a6b4a1785f75.json new file mode 100644 index 00000000..12c65eea --- /dev/null +++ b/.sqlx/query-2390acbe75f9956e8e16c29faa90aa2fb6b3e11a417302b62fc4a6b4a1785f75.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT version_id, field_id, int_value, enum_value, string_value\n FROM version_fields\n WHERE version_id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "field_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "int_value", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "enum_value", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "string_value", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + true, + true, + true + ] + }, + "hash": "2390acbe75f9956e8e16c29faa90aa2fb6b3e11a417302b62fc4a6b4a1785f75" +} diff --git a/.sqlx/query-2fe731da3681f72ec03b89d7139a49ccb1069079d8600daa40688d5f528de83d.json b/.sqlx/query-2fe731da3681f72ec03b89d7139a49ccb1069079d8600daa40688d5f528de83d.json new file mode 100644 index 00000000..5d9e7c19 --- /dev/null +++ b/.sqlx/query-2fe731da3681f72ec03b89d7139a49ccb1069079d8600daa40688d5f528de83d.json @@ -0,0 +1,173 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.description description, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n 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 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 FROM mods m \n INNER JOIN threads t ON t.mod_id = m.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 WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "summary", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "approved", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "queued", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "requested_status", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "organization_id", + "type_info": "Int8" + }, + { + "ordinal": 16, + "name": "license", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 18, + "name": "moderation_message", + "type_info": "Varchar" + }, + { + "ordinal": 19, + "name": "moderation_message_body", + "type_info": "Varchar" + }, + { + "ordinal": 20, + "name": "webhook_sent", + "type_info": "Bool" + }, + { + "ordinal": 21, + "name": "color", + "type_info": "Int4" + }, + { + "ordinal": 22, + "name": "thread_id", + "type_info": "Int8" + }, + { + "ordinal": 23, + "name": "monetization_status", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 25, + "name": "additional_categories", + "type_info": "VarcharArray" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + false, + true, + false, + true, + true, + true, + false, + true, + false, + false, + null, + null + ] + }, + "hash": "2fe731da3681f72ec03b89d7139a49ccb1069079d8600daa40688d5f528de83d" +} diff --git a/.sqlx/query-47611992348919d1f4ba21c35784492b95030d6c10d6a066b245b35be0386e81.json b/.sqlx/query-47611992348919d1f4ba21c35784492b95030d6c10d6a066b245b35be0386e81.json new file mode 100644 index 00000000..6559e9e5 --- /dev/null +++ b/.sqlx/query-47611992348919d1f4ba21c35784492b95030d6c10d6a066b245b35be0386e81.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id id, m.name name, m.summary summary, 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 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.is_owner = TRUE AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.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": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "summary", + "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" + } + ], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + false, + true, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "47611992348919d1f4ba21c35784492b95030d6c10d6a066b245b35be0386e81" +} diff --git a/.sqlx/query-4b9e5d78245ac083c167be708c196170c543a2157dbfa9d6249d98dc13bfaf72.json b/.sqlx/query-4b9e5d78245ac083c167be708c196170c543a2157dbfa9d6249d98dc13bfaf72.json deleted file mode 100644 index e9224fce..00000000 --- a/.sqlx/query-4b9e5d78245ac083c167be708c196170c543a2157dbfa9d6249d98dc13bfaf72.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.name name, 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 'version_id', 0, -- TODO: When webhook is updated to match others, this should match version\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.is_owner = TRUE 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": "name", - "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" - ] - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - false, - true, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "4b9e5d78245ac083c167be708c196170c543a2157dbfa9d6249d98dc13bfaf72" -} diff --git a/.sqlx/query-5329254eeb1e80d2a0f4f3bc2b613f3a7d54b0673f1a41f31fe5b5bbc4b5e478.json b/.sqlx/query-5329254eeb1e80d2a0f4f3bc2b613f3a7d54b0673f1a41f31fe5b5bbc4b5e478.json new file mode 100644 index 00000000..c869cb7b --- /dev/null +++ b/.sqlx/query-5329254eeb1e80d2a0f4f3bc2b613f3a7d54b0673f1a41f31fe5b5bbc4b5e478.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional\n FROM loader_fields lf\n WHERE id = ANY($1) \n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "field", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "field_type", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "enum_type", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "min_val", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "max_val", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "optional", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int4Array" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + false + ] + }, + "hash": "5329254eeb1e80d2a0f4f3bc2b613f3a7d54b0673f1a41f31fe5b5bbc4b5e478" +} diff --git a/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json b/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json deleted file mode 100644 index aa1d25f4..00000000 --- a/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH version_fields_cte AS (\n SELECT version_id, field_id, int_value, enum_value, string_value\n FROM version_fields WHERE version_id = ANY($1) \n ),\n\t\t\t\tversion_fields_json AS (\n\t\t\t\t\tSELECT DISTINCT version_id,\n JSONB_AGG( \n DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value)\n ) version_fields_json\n FROM version_fields_cte\n GROUP BY version_id\n\t\t\t\t),\n\t\t\t\tloader_fields_cte AS (\n\t\t\t\t\tSELECT DISTINCT vf.version_id, lf.*, l.loader\n\t\t\t\t\tFROM loader_fields lf\n INNER JOIN version_fields_cte vf ON lf.id = vf.field_id\n\t\t\t\t\tLEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id\n\t\t\t\t\tLEFT JOIN loaders l ON lv.loader_id = l.id\n GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader\n\t\t\t\t),\n loader_fields_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', lf.version_id,\n 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional\n )\n ) filter (where lf.id is not null) loader_fields_json\n FROM loader_fields_cte lf\n GROUP BY version_id\n ),\n loader_field_enum_values_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values_json\n FROM loader_field_enum_values lfev\n INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id\n GROUP BY version_id\n ),\n files_cte AS (\n SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type\n FROM files f\n WHERE f.version_id = ANY($1)\n ),\n files_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object('id', id, 'url', url, 'filename', filename, 'primary', is_primary, 'size', size, 'file_type', file_type)\n ) files_json\n FROM files_cte lf\n GROUP BY version_id\n ),\n hashes_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object('algorithm', algorithm, 'hash', encode(hash, 'escape'), 'file_id', file_id)\n ) hashes_json\n FROM hashes\n INNER JOIN files_cte lf on lf.id = hashes.file_id\n GROUP BY version_id\n ),\n dependencies_json AS (\n SELECT DISTINCT dependent_id as version_id,\n JSONB_AGG(\n 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)\n ) dependencies_json\n FROM dependencies d\n WHERE dependent_id = ANY($1)\n GROUP BY version_id\n )\n\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 f.files_json files,\n h.hashes_json hashes,\n d.dependencies_json dependencies,\n vf.version_fields_json version_fields,\n lf.loader_fields_json loader_fields,\n lfev.loader_field_enum_values_json loader_field_enum_values\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_json f on v.id = f.version_id\n LEFT OUTER JOIN hashes_json h on v.id = h.version_id\n LEFT OUTER JOIN dependencies_json d on v.id = d.version_id\n LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id\n LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id\n LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id\n WHERE v.id = ANY($1)\n GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, f.files_json, h.hashes_json, d.dependencies_json\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "version_name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "version_type", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "featured", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "requested_status", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "ordering", - "type_info": "Int4" - }, - { - "ordinal": 13, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 14, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 15, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 16, - "name": "files", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "hashes", - "type_info": "Jsonb" - }, - { - "ordinal": 18, - "name": "dependencies", - "type_info": "Jsonb" - }, - { - "ordinal": 19, - "name": "version_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "loader_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 21, - "name": "loader_field_enum_values", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e" -} diff --git a/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json b/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json new file mode 100644 index 00000000..63d3d400 --- /dev/null +++ b/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE collections\n SET updated = NOW()\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7" +} diff --git a/.sqlx/query-6d867e712d89c915fc15940eadded0a383aa479e7f25f3a408661347e35c6538.json b/.sqlx/query-6d867e712d89c915fc15940eadded0a383aa479e7f25f3a408661347e35c6538.json new file mode 100644 index 00000000..1b275596 --- /dev/null +++ b/.sqlx/query-6d867e712d89c915fc15940eadded0a383aa479e7f25f3a408661347e35c6538.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT file_id, algorithm, encode(hash, 'escape') hash\n FROM hashes\n WHERE file_id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "file_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "algorithm", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "6d867e712d89c915fc15940eadded0a383aa479e7f25f3a408661347e35c6538" +} diff --git a/.sqlx/query-777b3dcb5f45db64393476b0f9401e2ba04e59229c93c2768167253ea30abb32.json b/.sqlx/query-777b3dcb5f45db64393476b0f9401e2ba04e59229c93c2768167253ea30abb32.json new file mode 100644 index 00000000..523b4108 --- /dev/null +++ b/.sqlx/query-777b3dcb5f45db64393476b0f9401e2ba04e59229c93c2768167253ea30abb32.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT mod_id,\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 FROM versions v\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n INNER JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n INNER JOIN games g ON lptg.game_id = g.id\n WHERE v.id = ANY($1)\n GROUP BY mod_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 2, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 3, + "name": "games", + "type_info": "VarcharArray" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + null, + null, + null + ] + }, + "hash": "777b3dcb5f45db64393476b0f9401e2ba04e59229c93c2768167253ea30abb32" +} diff --git a/.sqlx/query-7bb8a2e1e01817ea3778fcd2af039e38d085484dd20abf57d0eff8d7801b728b.json b/.sqlx/query-7bb8a2e1e01817ea3778fcd2af039e38d085484dd20abf57d0eff8d7801b728b.json new file mode 100644 index 00000000..fdb571de --- /dev/null +++ b/.sqlx/query-7bb8a2e1e01817ea3778fcd2af039e38d085484dd20abf57d0eff8d7801b728b.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT mod_id, mg.image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering\n FROM mods_gallery mg\n INNER JOIN mods m ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "image_url", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "ordering", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + }, + "nullable": [ + false, + false, + true, + true, + true, + false, + false + ] + }, + "hash": "7bb8a2e1e01817ea3778fcd2af039e38d085484dd20abf57d0eff8d7801b728b" +} diff --git a/.sqlx/query-82d3a8a3bb864cbeda459065f7d4e413ffefa607a47be92b592d465b15b61006.json b/.sqlx/query-82d3a8a3bb864cbeda459065f7d4e413ffefa607a47be92b592d465b15b61006.json new file mode 100644 index 00000000..5a9b6822 --- /dev/null +++ b/.sqlx/query-82d3a8a3bb864cbeda459065f7d4e413ffefa607a47be92b592d465b15b61006.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT version_id,\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 FROM versions v\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n INNER JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n INNER JOIN games g ON lptg.game_id = g.id\n WHERE v.id = ANY($1)\n GROUP BY version_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 2, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 3, + "name": "games", + "type_info": "VarcharArray" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + null, + null, + null + ] + }, + "hash": "82d3a8a3bb864cbeda459065f7d4e413ffefa607a47be92b592d465b15b61006" +} diff --git a/.sqlx/query-834be4337c2dcc2a5f38c0f4ae0a2065b5a30fc43bb32ccfe8d58e9f3da24937.json b/.sqlx/query-834be4337c2dcc2a5f38c0f4ae0a2065b5a30fc43bb32ccfe8d58e9f3da24937.json deleted file mode 100644 index e1cc97f1..00000000 --- a/.sqlx/query-834be4337c2dcc2a5f38c0f4ae0a2065b5a30fc43bb32ccfe8d58e9f3da24937.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.is_owner = TRUE\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "834be4337c2dcc2a5f38c0f4ae0a2065b5a30fc43bb32ccfe8d58e9f3da24937" -} diff --git a/.sqlx/query-8615354803791e238cc037b8a105008014ecd9764d198e62cc1ad18fc3185301.json b/.sqlx/query-8615354803791e238cc037b8a105008014ecd9764d198e62cc1ad18fc3185301.json new file mode 100644 index 00000000..5489c2b5 --- /dev/null +++ b/.sqlx/query-8615354803791e238cc037b8a105008014ecd9764d198e62cc1ad18fc3185301.json @@ -0,0 +1,94 @@ +{ + "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 FROM versions v\n WHERE v.id = ANY($1)\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "version_type", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "requested_status", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "ordering", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "8615354803791e238cc037b8a105008014ecd9764d198e62cc1ad18fc3185301" +} diff --git a/.sqlx/query-8ff710a212087299ecc176ecc3cffbe5f411e76909ea458a359b9eea2c543e47.json b/.sqlx/query-8ff710a212087299ecc176ecc3cffbe5f411e76909ea458a359b9eea2c543e47.json new file mode 100644 index 00000000..082ad636 --- /dev/null +++ b/.sqlx/query-8ff710a212087299ecc176ecc3cffbe5f411e76909ea458a359b9eea2c543e47.json @@ -0,0 +1,47 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT joining_mod_id as mod_id, joining_platform_id as platform_id, lp.name as platform_name, url, lp.donation as donation\n FROM mods_links ml\n INNER JOIN mods m ON ml.joining_mod_id = m.id \n INNER JOIN link_platforms lp ON ml.joining_platform_id = lp.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "platform_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "platform_name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "donation", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "8ff710a212087299ecc176ecc3cffbe5f411e76909ea458a359b9eea2c543e47" +} diff --git a/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json b/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json new file mode 100644 index 00000000..9af819ae --- /dev/null +++ b/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT t.id\n FROM threads t\n INNER JOIN reports r ON t.report_id = r.id\n WHERE r.mod_id = $1 AND report_id IS NOT NULL \n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b" +} diff --git a/.sqlx/query-99080d0666e06794e44c80e05b17585e0f87c70d9ace28537898f27e7df0ded0.json b/.sqlx/query-99080d0666e06794e44c80e05b17585e0f87c70d9ace28537898f27e7df0ded0.json new file mode 100644 index 00000000..5d70c257 --- /dev/null +++ b/.sqlx/query-99080d0666e06794e44c80e05b17585e0f87c70d9ace28537898f27e7df0ded0.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT id, enum_id, value, ordering, created, metadata\n FROM loader_field_enum_values lfev\n WHERE id = ANY($1) \n ORDER BY enum_id, ordering, created ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "enum_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "value", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "ordering", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "metadata", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int4Array" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + true + ] + }, + "hash": "99080d0666e06794e44c80e05b17585e0f87c70d9ace28537898f27e7df0ded0" +} diff --git a/.sqlx/query-b94d2551866c355159d01f77fe301b191de2a83d3ba3817ea60628a1b45a7a64.json b/.sqlx/query-b94d2551866c355159d01f77fe301b191de2a83d3ba3817ea60628a1b45a7a64.json new file mode 100644 index 00000000..9c8ffbaf --- /dev/null +++ b/.sqlx/query-b94d2551866c355159d01f77fe301b191de2a83d3ba3817ea60628a1b45a7a64.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT dependent_id as version_id, d.mod_dependency_id as dependency_project_id, d.dependency_id as dependency_version_id, d.dependency_file_name as file_name, d.dependency_type as dependency_type\n FROM dependencies d\n WHERE dependent_id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "dependency_project_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "dependency_version_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "file_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "dependency_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + true, + true, + true, + false + ] + }, + "hash": "b94d2551866c355159d01f77fe301b191de2a83d3ba3817ea60628a1b45a7a64" +} diff --git a/.sqlx/query-ca53a711735ba065d441356ed744a95e948354bb5b9a6047749fdc2a514f456c.json b/.sqlx/query-ca53a711735ba065d441356ed744a95e948354bb5b9a6047749fdc2a514f456c.json new file mode 100644 index 00000000..6f4550b9 --- /dev/null +++ b/.sqlx/query-ca53a711735ba065d441356ed744a95e948354bb5b9a6047749fdc2a514f456c.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT mod_id, version_id, field_id, int_value, enum_value, string_value\n FROM versions v\n INNER JOIN version_fields vf ON v.id = vf.version_id\n WHERE v.id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "field_id", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "int_value", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "enum_value", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "string_value", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + true + ] + }, + "hash": "ca53a711735ba065d441356ed744a95e948354bb5b9a6047749fdc2a514f456c" +} diff --git a/.sqlx/query-cd564263de068c5e6e4b5f32587c65fa62d431aa0d7130427f27a809457be33e.json b/.sqlx/query-cd564263de068c5e6e4b5f32587c65fa62d431aa0d7130427f27a809457be33e.json new file mode 100644 index 00000000..8f20c07c --- /dev/null +++ b/.sqlx/query-cd564263de068c5e6e4b5f32587c65fa62d431aa0d7130427f27a809457be33e.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND is_owner = TRUE)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "cd564263de068c5e6e4b5f32587c65fa62d431aa0d7130427f27a809457be33e" +} diff --git a/.sqlx/query-e72736bb7fca4df41cf34186b1edf04d6b4d496971aaf87ed1a88e7d64eab823.json b/.sqlx/query-e72736bb7fca4df41cf34186b1edf04d6b4d496971aaf87ed1a88e7d64eab823.json new file mode 100644 index 00000000..20c4ed62 --- /dev/null +++ b/.sqlx/query-e72736bb7fca4df41cf34186b1edf04d6b4d496971aaf87ed1a88e7d64eab823.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type\n FROM files f\n WHERE f.version_id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "filename", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "is_primary", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "size", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "file_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true + ] + }, + "hash": "e72736bb7fca4df41cf34186b1edf04d6b4d496971aaf87ed1a88e7d64eab823" +} diff --git a/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json b/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json index f117b90b..c1b79a18 100644 --- a/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json +++ b/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json @@ -62,7 +62,7 @@ "nullable": [ false, false, - false, + true, true, true, false, diff --git a/.sqlx/query-f46c0ba514d4fa192b5d740b0ba6111ecaec51a0a23ac390d25214e2f3fb5cca.json b/.sqlx/query-f46c0ba514d4fa192b5d740b0ba6111ecaec51a0a23ac390d25214e2f3fb5cca.json deleted file mode 100644 index 091fa160..00000000 --- a/.sqlx/query-f46c0ba514d4fa192b5d740b0ba6111ecaec51a0a23ac390d25214e2f3fb5cca.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH version_fields_cte AS (\n SELECT mod_id, version_id, field_id, int_value, enum_value, string_value\n FROM mods m\n INNER JOIN versions v ON m.id = v.mod_id\n INNER JOIN version_fields vf ON v.id = vf.version_id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ),\n\t\t\t\tversion_fields_json AS (\n\t\t\t\t\tSELECT DISTINCT mod_id,\n JSONB_AGG( \n DISTINCT jsonb_build_object('version_id', version_id, 'field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value)\n ) version_fields_json\n FROM version_fields_cte\n GROUP BY mod_id\n\t\t\t\t),\n\t\t\t\tloader_fields_cte AS (\n\t\t\t\t\tSELECT DISTINCT vf.mod_id, vf.version_id, lf.*, l.loader\n\t\t\t\t\tFROM loader_fields lf\n INNER JOIN version_fields_cte vf ON lf.id = vf.field_id\n\t\t\t\t\tLEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id\n\t\t\t\t\tLEFT JOIN loaders l ON lv.loader_id = l.id\n GROUP BY vf.mod_id, vf.version_id, lf.enum_type, lf.id, l.loader\n\t\t\t\t),\n loader_fields_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', lf.version_id,\n 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional\n )\n ) filter (where lf.id is not null) loader_fields_json\n FROM loader_fields_cte lf\n GROUP BY mod_id\n ),\n loader_field_enum_values_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values_json\n FROM loader_field_enum_values lfev\n INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id\n GROUP BY mod_id\n ),\n versions_cte AS (\n SELECT DISTINCT mod_id, v.id as id, date_published\n FROM mods m\n INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3)\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ),\n versions_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', id, 'date_published', date_published\n )\n ) filter (where id is not null) versions_json\n FROM versions_cte\n GROUP BY mod_id\n ),\n loaders_cte AS (\n SELECT DISTINCT mod_id, l.id as id, l.loader\n FROM versions_cte\n INNER JOIN loaders_versions lv ON versions_cte.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id \n ),\n mods_gallery_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'image_url', mg.image_url, 'featured', mg.featured, 'name', mg.name, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering\n )\n ) filter (where image_url is not null) mods_gallery_json\n FROM mods_gallery mg\n INNER JOIN mods m ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY mod_id\n ),\n links_json AS (\n SELECT DISTINCT joining_mod_id as mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'platform_id', ml.joining_platform_id, 'platform_name', lp.name,'url', ml.url, 'donation', lp.donation\n )\n ) filter (where ml.joining_platform_id is not null) links_json\n FROM mods_links ml\n INNER JOIN mods m ON ml.joining_mod_id = m.id AND m.id = ANY($1) OR m.slug = ANY($2)\n INNER JOIN link_platforms lp ON ml.joining_platform_id = lp.id\n GROUP BY mod_id\n )\n \n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.description description, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n 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 v.versions_json versions,\n mg.mods_gallery_json gallery,\n ml.links_json links,\n vf.version_fields_json version_fields,\n lf.loader_fields_json loader_fields,\n lfev.loader_field_enum_values_json loader_field_enum_values\n FROM mods m \n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery_json mg ON mg.mod_id = m.id\n LEFT JOIN links_json ml ON ml.mod_id = m.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_json v ON v.mod_id = m.id\n LEFT JOIN loaders_cte l on l.mod_id = m.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 LEFT OUTER JOIN version_fields_json vf ON m.id = vf.mod_id\n LEFT OUTER JOIN loader_fields_json lf ON m.id = lf.mod_id\n LEFT OUTER JOIN loader_field_enum_values_json lfev ON m.id = lfev.mod_id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id, version_fields_json, loader_fields_json, loader_field_enum_values_json, versions_json, mods_gallery_json, links_json;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "summary", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "approved", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "queued", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "requested_status", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 15, - "name": "organization_id", - "type_info": "Int8" - }, - { - "ordinal": 16, - "name": "license", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "moderation_message", - "type_info": "Varchar" - }, - { - "ordinal": 19, - "name": "moderation_message_body", - "type_info": "Varchar" - }, - { - "ordinal": 20, - "name": "webhook_sent", - "type_info": "Bool" - }, - { - "ordinal": 21, - "name": "color", - "type_info": "Int4" - }, - { - "ordinal": 22, - "name": "thread_id", - "type_info": "Int8" - }, - { - "ordinal": 23, - "name": "monetization_status", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 25, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 26, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 27, - "name": "categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 28, - "name": "additional_categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 29, - "name": "versions", - "type_info": "Jsonb" - }, - { - "ordinal": 30, - "name": "gallery", - "type_info": "Jsonb" - }, - { - "ordinal": 31, - "name": "links", - "type_info": "Jsonb" - }, - { - "ordinal": 32, - "name": "version_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 33, - "name": "loader_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 34, - "name": "loader_field_enum_values", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray", - "TextArray" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - false, - true, - false, - true, - true, - true, - false, - true, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "f46c0ba514d4fa192b5d740b0ba6111ecaec51a0a23ac390d25214e2f3fb5cca" -} diff --git a/.sqlx/query-f73a0a6a79f97213477fc862101d0ced00500ab81336d129b5621581e9cd5e62.json b/.sqlx/query-f73a0a6a79f97213477fc862101d0ced00500ab81336d129b5621581e9cd5e62.json deleted file mode 100644 index d24f2e9a..00000000 --- a/.sqlx/query-f73a0a6a79f97213477fc862101d0ced00500ab81336d129b5621581e9cd5e62.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND is_owner = TRUE)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "f73a0a6a79f97213477fc862101d0ced00500ab81336d129b5621581e9cd5e62" -} diff --git a/Cargo.lock b/Cargo.lock index 70c971d5..f85608f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,12 +427,6 @@ dependencies = [ "password-hash 0.5.0", ] -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.4" @@ -2208,12 +2202,12 @@ dependencies = [ ] [[package]] -name = "iso8601-duration" -version = "0.1.0" +name = "iso8601" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" dependencies = [ - "nom 5.1.3", + "nom", ] [[package]] @@ -2404,26 +2398,13 @@ dependencies = [ "idna 0.3.0", "mime", "native-tls", - "nom 7.1.3", + "nom", "once_cell", "quoted_printable", "socket2 0.4.10", "tokio", ] -[[package]] -name = "lexical-core" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" -dependencies = [ - "arrayvec 0.5.2", - "bitflags 1.3.2", - "cfg-if", - "ryu", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.150" @@ -2601,9 +2582,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "meilisearch-index-setting-macro" -version = "0.22.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fedd7e2fabfbcc91679f3d76f6d648ea7fc9ea87c841b10d26c2a258f408da" +checksum = "b1f2124b55b9cb28e6a08b28854f4e834a51333cbdc2f72935f401efa686c13c" dependencies = [ "convert_case 0.6.0", "proc-macro2", @@ -2613,16 +2594,16 @@ dependencies = [ [[package]] name = "meilisearch-sdk" -version = "0.22.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6be928c91e1b23689725586b56f3284f394d93185accfa2771caec3e10015d" +checksum = "2257ea8ed24b079c21570f473e58cccc3de23b46cee331fc513fccdc3f1ae5a1" dependencies = [ "async-trait", "either", "futures", "futures-io", "isahc", - "iso8601-duration", + "iso8601", "js-sys", "jsonwebtoken", "log", @@ -2724,17 +2705,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "lexical-core", - "memchr", - "version_check", -] - [[package]] name = "nom" version = "7.1.3" @@ -3060,7 +3030,7 @@ dependencies = [ "fnv", "itertools 0.11.0", "lazy_static", - "nom 7.1.3", + "nom", "quick-xml 0.28.2", "regex", "regex-cache", @@ -3660,7 +3630,7 @@ version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ - "arrayvec 0.7.4", + "arrayvec", "borsh", "bytes", "num-traits", @@ -4246,7 +4216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ "itertools 0.11.0", - "nom 7.1.3", + "nom", "unicode_categories", ] diff --git a/Cargo.toml b/Cargo.toml index 83cb9d6a..09f98887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ async-trait = "0.1.70" dashmap = "5.4.0" lazy_static = "1.4.0" -meilisearch-sdk = "0.22.0" +meilisearch-sdk = "0.24.3" rust-s3 = "0.33.0" reqwest = { version = "0.11.18", features = ["json", "multipart"] } hyper = { version = "0.14", features = ["full"] } diff --git a/docker-compose.yml b/docker-compose.yml index 1fc25bca..11a4f806 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,12 +12,12 @@ services: POSTGRES_PASSWORD: labrinth POSTGRES_HOST_AUTH_METHOD: trust meilisearch: - image: getmeili/meilisearch:v1.0.1 + image: getmeili/meilisearch:v1.5.0 restart: on-failure ports: - "7700:7700" volumes: - - meilisearch-data:/meili_data + - meilisearch-data:/data.ms environment: MEILI_MASTER_KEY: modrinth redis: diff --git a/migrations/20231205095400_remaining_loader_field_loaders.sql b/migrations/20231205095400_remaining_loader_field_loaders.sql new file mode 100644 index 00000000..6ff87804 --- /dev/null +++ b/migrations/20231205095400_remaining_loader_field_loaders.sql @@ -0,0 +1,16 @@ +-- Adds loader_fields_loaders entries for all loaders +-- (at this point, they are all Minecraft loaders, and thus have the same fields) +-- These are loaders such as bukkit, minecraft, vanilla, waterfall, velocity... etc +-- This also allows v2 routes (which have things such as client_side to remain to work with these loaders) +INSERT INTO loader_fields_loaders +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf +WHERE lf.field=ANY(ARRAY['game_versions','client_and_server','server_only','client_only','singleplayer']) +AND +l.loader NOT IN ('vanilla', 'minecraft', 'optifine', 'iris', 'canvas') +ON CONFLICT DO NOTHING; + +-- All existing loader_project_types so far should have a games entry as minecraft +INSERT INTO loaders_project_types_games +SELECT lpt.joining_loader_id, lpt.joining_project_type_id, g.id FROM loaders_project_types lpt CROSS JOIN games g +WHERE g.name='minecraft-java' +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/migrations/20231211184922_collections_description_nullable.sql b/migrations/20231211184922_collections_description_nullable.sql new file mode 100644 index 00000000..08b39c3e --- /dev/null +++ b/migrations/20231211184922_collections_description_nullable.sql @@ -0,0 +1 @@ +ALTER TABLE collections ALTER COLUMN description DROP NOT NULL; \ No newline at end of file diff --git a/src/auth/checks.rs b/src/auth/checks.rs index 4d47e72c..20733515 100644 --- a/src/auth/checks.rs +++ b/src/auth/checks.rs @@ -2,6 +2,7 @@ use crate::database; use crate::database::models::project_item::QueryProject; use crate::database::models::version_item::QueryVersion; use crate::database::models::Collection; +use crate::database::redis::RedisPool; use crate::database::{models, Project, Version}; use crate::models::users::User; use crate::routes::ApiError; @@ -175,17 +176,17 @@ pub async fn is_authorized_version( Ok(authorized) } -impl ValidateAuthorized for crate::database::models::OAuthClient { +impl ValidateAuthorized for models::OAuthClient { fn validate_authorized(&self, user_option: Option<&User>) -> Result<(), ApiError> { if let Some(user) = user_option { - if user.role.is_mod() || user.id == self.created_by.into() { - return Ok(()); + return if user.role.is_mod() || user.id == self.created_by.into() { + Ok(()) } else { - return Err(crate::routes::ApiError::CustomAuthentication( + Err(ApiError::CustomAuthentication( "You don't have sufficient permissions to interact with this OAuth application" .to_string(), - )); - } + )) + }; } Ok(()) @@ -196,9 +197,23 @@ pub async fn filter_authorized_versions( versions: Vec, user_option: &Option, pool: &web::Data, + redis: web::Data, ) -> Result, ApiError> { let mut return_versions = Vec::new(); - let mut check_versions = Vec::new(); + + let project_ids = versions + .iter() + .map(|x| x.inner.project_id) + .collect::>(); + + let authorized_projects = filter_authorized_projects( + Project::get_many_ids(&project_ids, &***pool, &redis).await?, + user_option, + pool, + ) + .await?; + + let authorized_project_ids: Vec<_> = authorized_projects.iter().map(|x| x.id.into()).collect(); for version in versions { if !version.inner.status.is_hidden() @@ -206,48 +221,9 @@ pub async fn filter_authorized_versions( .as_ref() .map(|x| x.role.is_mod()) .unwrap_or(false) + || (user_option.is_some() && authorized_project_ids.contains(&version.inner.project_id)) { return_versions.push(version.into()); - } else if user_option.is_some() { - check_versions.push(version); - } - } - - if !check_versions.is_empty() { - if let Some(user) = user_option { - let user_id: models::ids::UserId = user.id.into(); - - use futures::TryStreamExt; - - sqlx::query!( - " - SELECT m.id FROM mods m - INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 - WHERE m.id = ANY($1) - ", - &check_versions - .iter() - .map(|x| x.inner.project_id.0) - .collect::>(), - user_id as database::models::ids::UserId, - ) - .fetch_many(&***pool) - .try_for_each(|e| { - if let Some(row) = e.right() { - check_versions.retain(|x| { - let bool = x.inner.project_id.0 == row.id; - - if bool { - return_versions.push(x.clone().into()); - } - - !bool - }); - } - - futures::future::ready(Ok(())) - }) - .await?; } } diff --git a/src/database/models/collection_item.rs b/src/database/models/collection_item.rs index 035fad6b..a2c29283 100644 --- a/src/database/models/collection_item.rs +++ b/src/database/models/collection_item.rs @@ -13,7 +13,7 @@ pub struct CollectionBuilder { pub collection_id: CollectionId, pub user_id: UserId, pub name: String, - pub description: String, + pub description: Option, pub status: CollectionStatus, pub projects: Vec, } @@ -45,7 +45,7 @@ pub struct Collection { pub id: CollectionId, pub user_id: UserId, pub name: String, - pub description: String, + pub description: Option, pub created: DateTime, pub updated: DateTime, pub icon_url: Option, @@ -73,7 +73,7 @@ impl Collection { self.id as CollectionId, self.user_id as UserId, &self.name, - &self.description, + self.description.as_ref(), self.created, self.icon_url.as_ref(), self.status.to_string(), diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index d3b13f09..6bf20cba 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -228,6 +228,20 @@ impl LoaderFieldType { LoaderFieldType::ArrayEnum(_) => "array_enum", } } + + pub fn is_array(&self) -> bool { + match self { + LoaderFieldType::ArrayInteger => true, + LoaderFieldType::ArrayText => true, + LoaderFieldType::ArrayBoolean => true, + LoaderFieldType::ArrayEnum(_) => true, + + LoaderFieldType::Integer => false, + LoaderFieldType::Text => false, + LoaderFieldType::Boolean => false, + LoaderFieldType::Enum(_) => false, + } + } } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -283,7 +297,7 @@ pub struct QueryVersionField { pub version_id: VersionId, pub field_id: LoaderFieldId, pub int_value: Option, - pub enum_value: Option, + pub enum_value: Option, pub string_value: Option, } @@ -293,7 +307,7 @@ impl QueryVersionField { self } - pub fn with_enum_value(mut self, enum_value: LoaderFieldEnumValue) -> Self { + pub fn with_enum_value(mut self, enum_value: LoaderFieldEnumValueId) -> Self { self.enum_value = Some(enum_value); self } @@ -304,6 +318,27 @@ impl QueryVersionField { } } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct QueryLoaderField { + pub id: LoaderFieldId, + pub field: String, + pub field_type: String, + pub enum_type: Option, + pub min_val: Option, + pub max_val: Option, + pub optional: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct QueryLoaderFieldEnumValue { + pub id: LoaderFieldEnumValueId, + pub enum_id: LoaderFieldEnumId, + pub value: String, + pub ordering: Option, + pub created: DateTime, + pub metadata: Option, +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct SideType { pub id: SideTypeId, @@ -710,11 +745,11 @@ impl VersionField { } } VersionFieldValue::Enum(_, v) => { - query_version_fields.push(base.clone().with_enum_value(v)) + query_version_fields.push(base.clone().with_enum_value(v.id)) } VersionFieldValue::ArrayEnum(_, v) => { for ev in v { - query_version_fields.push(base.clone().with_enum_value(ev)); + query_version_fields.push(base.clone().with_enum_value(ev.id)); } } }; @@ -733,7 +768,7 @@ impl VersionField { l.field_id.0, l.version_id.0, l.int_value, - l.enum_value.as_ref().map(|e| e.id.0), + l.enum_value.as_ref().map(|e| e.0), l.string_value.clone(), ) }) @@ -807,106 +842,53 @@ impl VersionField { } pub fn from_query_json( - loader_fields: Option, - version_fields: Option, - loader_field_enum_values: Option, + query_version_field_combined: Vec, + query_loader_fields: &[QueryLoaderField], + query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], allow_many: bool, // If true, will allow multiple values for a single singleton field, returning them as separate VersionFields // allow_many = true, multiple Bools => two VersionFields of Bool // allow_many = false, multiple Bools => error // multiple Arraybools => 1 VersionField of ArrayBool ) -> Vec { - #[derive(Deserialize, Debug)] - struct JsonLoaderField { - version_id: i64, - - lf_id: i32, - field: String, - field_type: String, - enum_type: Option, - min_val: Option, - max_val: Option, - optional: bool, - } - - #[derive(Deserialize, Debug)] - struct JsonVersionField { - field_id: i32, - int_value: Option, - enum_value: Option, - string_value: Option, - } - - #[derive(Deserialize, Debug)] - struct JsonLoaderFieldEnumValue { - id: i32, - enum_id: i32, - value: String, - ordering: Option, - created: DateTime, - metadata: Option, - } - - let query_loader_fields: Vec = loader_fields - .and_then(|x| serde_json::from_value(x).ok()) - .unwrap_or_default(); - let query_version_field_combined: Vec = version_fields - .and_then(|x| serde_json::from_value(x).ok()) - .unwrap_or_default(); - let query_loader_field_enum_values: Vec = - loader_field_enum_values - .and_then(|x| serde_json::from_value(x).ok()) - .unwrap_or_default(); query_loader_fields - .into_iter() + .iter() .flat_map(|q| { - let loader_field_type = match LoaderFieldType::build(&q.field_type, q.enum_type) { - Some(lft) => lft, - None => return vec![], - }; + let loader_field_type = + match LoaderFieldType::build(&q.field_type, q.enum_type.map(|l| l.0)) { + Some(lft) => lft, + None => return vec![], + }; let loader_field = LoaderField { - id: LoaderFieldId(q.lf_id), + id: q.id, field: q.field.clone(), field_type: loader_field_type, optional: q.optional, min_val: q.min_val, max_val: q.max_val, }; - let version_id = VersionId(q.version_id); - let values = query_version_field_combined + + // todo: avoid clone here? + let version_fields = query_version_field_combined .iter() - .filter_map(|qvf| { - if qvf.field_id == q.lf_id { - let lfev = query_loader_field_enum_values - .iter() - .find(|x| Some(x.id) == qvf.enum_value); - - Some(QueryVersionField { - version_id, - field_id: LoaderFieldId(qvf.field_id), - int_value: qvf.int_value, - enum_value: lfev.map(|lfev| LoaderFieldEnumValue { - id: LoaderFieldEnumValueId(lfev.id), - enum_id: LoaderFieldEnumId(lfev.enum_id), - value: lfev.value.clone(), - ordering: lfev.ordering, - created: lfev.created, - metadata: lfev.metadata.clone().unwrap_or_default(), - }), - string_value: qvf.string_value.clone(), - }) - } else { - None - } - }) + .filter(|qvf| qvf.field_id == q.id) + .cloned() .collect::>(); if allow_many { - VersionField::build_many(loader_field, version_id, values) - .unwrap_or_default() - .into_iter() - .unique() - .collect_vec() + VersionField::build_many( + loader_field, + version_fields, + query_loader_field_enum_values, + ) + .unwrap_or_default() + .into_iter() + .unique() + .collect_vec() } else { - match VersionField::build(loader_field, version_id, values) { + match VersionField::build( + loader_field, + version_fields, + query_loader_field_enum_values, + ) { Ok(vf) => vec![vf], Err(_) => vec![], } @@ -917,10 +899,14 @@ impl VersionField { pub fn build( loader_field: LoaderField, - version_id: VersionId, query_version_fields: Vec, + query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], ) -> Result { - let value = VersionFieldValue::build(&loader_field.field_type, query_version_fields)?; + let (version_id, value) = VersionFieldValue::build( + &loader_field.field_type, + query_version_fields, + query_loader_field_enum_values, + )?; Ok(VersionField { version_id, field_id: loader_field.id, @@ -931,13 +917,17 @@ impl VersionField { pub fn build_many( loader_field: LoaderField, - version_id: VersionId, query_version_fields: Vec, + query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], ) -> Result, DatabaseError> { - let values = VersionFieldValue::build_many(&loader_field.field_type, query_version_fields)?; + let values = VersionFieldValue::build_many( + &loader_field.field_type, + query_version_fields, + query_loader_field_enum_values, + )?; Ok(values .into_iter() - .map(|value| VersionField { + .map(|(version_id, value)| VersionField { version_id, field_id: loader_field.id, field_name: loader_field.field.clone(), @@ -1030,13 +1020,14 @@ impl VersionFieldValue { pub fn build( field_type: &LoaderFieldType, qvfs: Vec, - ) -> Result { + qlfev: &[QueryLoaderFieldEnumValue], + ) -> Result<(VersionId, VersionFieldValue), DatabaseError> { match field_type { LoaderFieldType::Integer | LoaderFieldType::Text | LoaderFieldType::Boolean | LoaderFieldType::Enum(_) => { - let mut fields = Self::build_many(field_type, qvfs)?; + let mut fields = Self::build_many(field_type, qvfs, qlfev)?; if fields.len() > 1 { return Err(DatabaseError::SchemaError(format!( "Multiple fields for field {}", @@ -1054,7 +1045,7 @@ impl VersionFieldValue { | LoaderFieldType::ArrayText | LoaderFieldType::ArrayBoolean | LoaderFieldType::ArrayEnum(_) => { - let fields = Self::build_many(field_type, qvfs)?; + let fields = Self::build_many(field_type, qvfs, qlfev)?; Ok(fields.into_iter().next().ok_or_else(|| { DatabaseError::SchemaError(format!( "No version fields for field {}", @@ -1066,14 +1057,15 @@ impl VersionFieldValue { } // Build from internal query data - // This encapsulates reundant behavior in db querie -> object conversions + // This encapsulates redundant behavior in db query -> object conversions // This allows for multiple fields to be built at once. If there are multiple fields, // but the type only allows for a single field, then multiple VersionFieldValues will be returned // If there are multiple fields, and the type allows for multiple fields, then a single VersionFieldValue will be returned (array.len == 1) pub fn build_many( field_type: &LoaderFieldType, qvfs: Vec, - ) -> Result, DatabaseError> { + qlfev: &[QueryLoaderFieldEnumValue], + ) -> Result, DatabaseError> { let field_name = field_type.to_str(); let did_not_exist_error = |field_name: &str, desired_field: &str| { DatabaseError::SchemaError(format!( @@ -1082,82 +1074,168 @@ impl VersionFieldValue { )) }; - Ok(match field_type { + // Check errors- version_id must all be the same + let version_id = qvfs + .iter() + .map(|qvf| qvf.version_id) + .unique() + .collect::>(); + // If the field type is a non-array, then the reason for multiple version ids is that there are multiple versions being aggregated, and those version ids are contained within. + // If the field type is an array, then the reason for multiple version ids is that there are multiple values for a single version + // (or a greater aggregation between multiple arrays, in which case the per-field version is lost, so we just take the first one and use it for that) + let version_id = version_id.into_iter().next().unwrap_or(VersionId(0)); + + let field_id = qvfs + .iter() + .map(|qvf| qvf.field_id) + .unique() + .collect::>(); + if field_id.len() > 1 { + return Err(DatabaseError::SchemaError(format!( + "Multiple field ids for field {}", + field_name + ))); + } + + let mut value = match field_type { + // Singleton fields + // If there are multiple, we assume multiple versions are being concatenated LoaderFieldType::Integer => qvfs .into_iter() .map(|qvf| { - Ok(VersionFieldValue::Integer( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))?, + Ok(( + qvf.version_id, + VersionFieldValue::Integer( + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value"))?, + ), )) }) - .collect::, DatabaseError>>()?, + .collect::, DatabaseError>>()?, LoaderFieldType::Text => qvfs .into_iter() .map(|qvf| { - Ok::(VersionFieldValue::Text( - qvf.string_value - .ok_or(did_not_exist_error(field_name, "string_value"))?, + Ok(( + qvf.version_id, + VersionFieldValue::Text( + qvf.string_value + .ok_or(did_not_exist_error(field_name, "string_value"))?, + ), )) }) - .collect::, DatabaseError>>()?, + .collect::, DatabaseError>>()?, LoaderFieldType::Boolean => qvfs .into_iter() .map(|qvf| { - Ok::(VersionFieldValue::Boolean( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))? - != 0, + Ok(( + qvf.version_id, + VersionFieldValue::Boolean( + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value"))? + != 0, + ), )) }) - .collect::, DatabaseError>>()?, + .collect::, DatabaseError>>()?, LoaderFieldType::Enum(id) => qvfs .into_iter() .map(|qvf| { - Ok::(VersionFieldValue::Enum( - *id, - qvf.enum_value - .ok_or(did_not_exist_error(field_name, "enum_value"))?, + Ok(( + qvf.version_id, + VersionFieldValue::Enum(*id, { + let enum_id = qvf + .enum_value + .ok_or(did_not_exist_error(field_name, "enum_value"))?; + let lfev = qlfev + .iter() + .find(|x| x.id == enum_id) + .ok_or(did_not_exist_error(field_name, "enum_value"))?; + LoaderFieldEnumValue { + id: lfev.id, + enum_id: lfev.enum_id, + value: lfev.value.clone(), + ordering: lfev.ordering, + created: lfev.created, + metadata: lfev.metadata.clone().unwrap_or_default(), + } + }), )) }) - .collect::, DatabaseError>>()?, - LoaderFieldType::ArrayInteger => vec![VersionFieldValue::ArrayInteger( - qvfs.into_iter() - .map(|qvf| { - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value")) - }) - .collect::>()?, + .collect::, DatabaseError>>()?, + + // Array fields + // We concatenate into one array + LoaderFieldType::ArrayInteger => vec![( + version_id, + VersionFieldValue::ArrayInteger( + qvfs.into_iter() + .map(|qvf| { + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value")) + }) + .collect::>()?, + ), )], - LoaderFieldType::ArrayText => vec![VersionFieldValue::ArrayText( - qvfs.into_iter() - .map(|qvf| { - qvf.string_value - .ok_or(did_not_exist_error(field_name, "string_value")) - }) - .collect::>()?, + LoaderFieldType::ArrayText => vec![( + version_id, + VersionFieldValue::ArrayText( + qvfs.into_iter() + .map(|qvf| { + qvf.string_value + .ok_or(did_not_exist_error(field_name, "string_value")) + }) + .collect::>()?, + ), )], - LoaderFieldType::ArrayBoolean => vec![VersionFieldValue::ArrayBoolean( - qvfs.into_iter() - .map(|qvf| { - Ok::( - qvf.int_value - .ok_or(did_not_exist_error(field_name, "int_value"))? - != 0, - ) - }) - .collect::>()?, + LoaderFieldType::ArrayBoolean => vec![( + version_id, + VersionFieldValue::ArrayBoolean( + qvfs.into_iter() + .map(|qvf| { + Ok::( + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value"))? + != 0, + ) + }) + .collect::>()?, + ), )], - LoaderFieldType::ArrayEnum(id) => vec![VersionFieldValue::ArrayEnum( - *id, - qvfs.into_iter() - .map(|qvf| { - qvf.enum_value - .ok_or(did_not_exist_error(field_name, "enum_value")) - }) - .collect::>()?, + LoaderFieldType::ArrayEnum(id) => vec![( + version_id, + VersionFieldValue::ArrayEnum( + *id, + qvfs.into_iter() + .map(|qvf| { + let enum_id = qvf + .enum_value + .ok_or(did_not_exist_error(field_name, "enum_value"))?; + let lfev = qlfev + .iter() + .find(|x| x.id == enum_id) + .ok_or(did_not_exist_error(field_name, "enum_value"))?; + Ok::<_, DatabaseError>(LoaderFieldEnumValue { + id: lfev.id, + enum_id: lfev.enum_id, + value: lfev.value.clone(), + ordering: lfev.ordering, + created: lfev.created, + metadata: lfev.metadata.clone().unwrap_or_default(), + }) + }) + .collect::>()?, + ), )], - }) + }; + + // Sort arrayenums by ordering, then by created + for (_, v) in value.iter_mut() { + if let VersionFieldValue::ArrayEnum(_, v) = v { + v.sort_by(|a, b| a.ordering.cmp(&b.ordering).then(a.created.cmp(&b.created))); + } + } + + Ok(value) } // Serialize to internal value, such as for converting to user-facing JSON diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index a66ff2ff..cd65bd0a 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -1,4 +1,6 @@ -use super::loader_fields::VersionField; +use super::loader_fields::{ + QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, VersionField, +}; use super::{ids::*, User}; use crate::database::models; use crate::database::models::DatabaseError; @@ -6,6 +8,7 @@ use crate::database::redis::RedisPool; use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::projects::{MonetizationStatus, ProjectStatus}; use chrono::{DateTime, Utc}; +use dashmap::{DashMap, DashSet}; use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -346,6 +349,28 @@ impl Project { .execute(&mut **transaction) .await?; + // Notably joins with report id and not thread.mod_id directly, as + // this is set to null for threads that are reports. + let report_threads = sqlx::query!( + " + SELECT t.id + FROM threads t + INNER JOIN reports r ON t.report_id = r.id + WHERE r.mod_id = $1 AND report_id IS NOT NULL + ", + id as ProjectId, + ) + .fetch_many(&mut **transaction) + .try_filter_map(|e| async { Ok(e.right().map(|x| ThreadId(x.id))) }) + .try_collect::>() + .await?; + + for thread_id in report_threads { + models::Thread::remove_full(thread_id, transaction).await?; + } + + models::Thread::remove_full(project.thread_id, transaction).await?; + sqlx::query!( " DELETE FROM reports @@ -400,8 +425,6 @@ impl Project { .execute(&mut **transaction) .await?; - models::Thread::remove_full(project.thread_id, transaction).await?; - sqlx::query!( " DELETE FROM mods @@ -451,7 +474,7 @@ impl Project { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { Project::get_many(&[string], executor, redis) .await @@ -464,7 +487,7 @@ impl Project { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { Project::get_many(&[crate::models::ids::ProjectId::from(id)], executor, redis) .await @@ -477,7 +500,7 @@ impl Project { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { let ids = project_ids .iter() @@ -492,19 +515,23 @@ impl Project { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { + let project_strings = project_strings + .iter() + .map(|x| x.to_string()) + .unique() + .collect::>(); + if project_strings.is_empty() { return Ok(Vec::new()); } let mut redis = redis.connect().await?; + let mut exec = exec.acquire().await?; let mut found_projects = Vec::new(); - let mut remaining_strings = project_strings - .iter() - .map(|x| x.to_string()) - .collect::>(); + let mut remaining_strings = project_strings.clone(); let mut project_ids = project_strings .iter() @@ -549,104 +576,204 @@ impl Project { .flat_map(|x| parse_base62(&x.to_string()).ok()) .map(|x| x as i64) .collect(); + let slugs = remaining_strings + .into_iter() + .map(|x| x.to_lowercase()) + .collect::>(); + + let all_version_ids = DashSet::new(); + let versions: DashMap)>> = sqlx::query!( + " + SELECT DISTINCT mod_id, v.id as id, date_published + FROM mods m + INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3) + WHERE m.id = ANY($1) OR m.slug = ANY($2) + ", + &project_ids_parsed, + &slugs, + &*crate::models::projects::VersionStatus::iterator() + .filter(|x| x.is_listed()) + .map(|x| x.to_string()) + .collect::>() + ) + .fetch(&mut *exec) + .try_fold( + DashMap::new(), + |acc: DashMap)>>, m| { + let version_id = VersionId(m.id); + let date_published = m.date_published; + all_version_ids.insert(version_id); + acc.entry(ProjectId(m.mod_id)) + .or_default() + .push((version_id, date_published)); + async move { Ok(acc) } + }, + ) + .await?; + + let loader_field_ids = DashSet::new(); + let loader_field_enum_value_ids = DashSet::new(); + let version_fields: DashMap> = sqlx::query!( + " + SELECT DISTINCT mod_id, version_id, field_id, int_value, enum_value, string_value + FROM versions v + INNER JOIN version_fields vf ON v.id = vf.version_id + WHERE v.id = ANY($1) + ", + &all_version_ids.iter().map(|x| x.0).collect::>() + ) + .fetch(&mut *exec) + .try_fold( + DashMap::new(), + |acc: DashMap>, m| { + let qvf = QueryVersionField { + version_id: VersionId(m.version_id), + field_id: LoaderFieldId(m.field_id), + int_value: m.int_value, + enum_value: m.enum_value.map(LoaderFieldEnumValueId), + string_value: m.string_value, + }; + + loader_field_ids.insert(LoaderFieldId(m.field_id)); + if let Some(enum_value) = m.enum_value { + loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(enum_value)); + } + + acc.entry(ProjectId(m.mod_id)).or_default().push(qvf); + async move { Ok(acc) } + }, + ) + .await?; + + let loader_fields: Vec = sqlx::query!( + " + SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional + FROM loader_fields lf + WHERE id = ANY($1) + ", + &loader_field_ids.iter().map(|x| x.0).collect::>() + ) + .fetch(&mut *exec) + .map_ok(|m| QueryLoaderField { + id: LoaderFieldId(m.id), + field: m.field, + field_type: m.field_type, + enum_type: m.enum_type.map(LoaderFieldEnumId), + min_val: m.min_val, + max_val: m.max_val, + optional: m.optional, + }) + .try_collect() + .await?; + + let loader_field_enum_values: Vec = sqlx::query!( + " + SELECT DISTINCT id, enum_id, value, ordering, created, metadata + FROM loader_field_enum_values lfev + WHERE id = ANY($1) + ORDER BY enum_id, ordering, created DESC + ", + &loader_field_enum_value_ids + .iter() + .map(|x| x.0) + .collect::>() + ) + .fetch(&mut *exec) + .map_ok(|m| QueryLoaderFieldEnumValue { + id: LoaderFieldEnumValueId(m.id), + enum_id: LoaderFieldEnumId(m.enum_id), + value: m.value, + ordering: m.ordering, + created: m.created, + metadata: m.metadata, + }) + .try_collect() + .await?; + + let mods_gallery: DashMap> = sqlx::query!( + " + SELECT DISTINCT mod_id, mg.image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering + FROM mods_gallery mg + INNER JOIN mods m ON mg.mod_id = m.id + WHERE m.id = ANY($1) OR m.slug = ANY($2) + ", + &project_ids_parsed, + &slugs + ).fetch(&mut *exec) + .try_fold(DashMap::new(), |acc : DashMap>, m| { + acc.entry(ProjectId(m.mod_id)) + .or_default() + .push(GalleryItem { + image_url: m.image_url, + featured: m.featured.unwrap_or(false), + name: m.name, + description: m.description, + created: m.created, + ordering: m.ordering, + }); + async move { Ok(acc) } + } + ).await?; + + let links: DashMap> = sqlx::query!( + " + SELECT DISTINCT joining_mod_id as mod_id, joining_platform_id as platform_id, lp.name as platform_name, url, lp.donation as donation + FROM mods_links ml + INNER JOIN mods m ON ml.joining_mod_id = m.id + INNER JOIN link_platforms lp ON ml.joining_platform_id = lp.id + WHERE m.id = ANY($1) OR m.slug = ANY($2) + ", + &project_ids_parsed, + &slugs + ).fetch(&mut *exec) + .try_fold(DashMap::new(), |acc : DashMap>, m| { + acc.entry(ProjectId(m.mod_id)) + .or_default() + .push(LinkUrl { + platform_id: LinkPlatformId(m.platform_id), + platform_name: m.platform_name, + url: m.url, + donation: m.donation, + }); + async move { Ok(acc) } + } + ).await?; + + type StringTriple = (Vec, Vec, Vec); + let loaders_ptypes_games: DashMap = sqlx::query!( + " + SELECT DISTINCT mod_id, + 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.slug) filter (where g.slug is not null) games + FROM versions v + INNER JOIN loaders_versions lv ON v.id = lv.version_id + INNER JOIN loaders l ON lv.loader_id = l.id + INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id + INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id + INNER JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id + INNER JOIN games g ON lptg.game_id = g.id + WHERE v.id = ANY($1) + GROUP BY mod_id + ", + &all_version_ids.iter().map(|x| x.0).collect::>() + ).fetch(&mut *exec) + .map_ok(|m| { + let project_id = ProjectId(m.mod_id); + let loaders = m.loaders.unwrap_or_default(); + let project_types = m.project_types.unwrap_or_default(); + let games = m.games.unwrap_or_default(); + + (project_id, (loaders, project_types, games)) + + } + ).try_collect().await?; // TODO: Possible improvements to look into: // - use multiple queries instead of CTES (for cleanliness?) // - repeated joins to mods in separate CTEs- perhaps 1 CTE for mods and use later (in mods_gallery_json, mods_donations_json, etc.) let db_projects: Vec = sqlx::query!( " - WITH version_fields_cte AS ( - SELECT mod_id, version_id, field_id, int_value, enum_value, string_value - FROM mods m - INNER JOIN versions v ON m.id = v.mod_id - INNER JOIN version_fields vf ON v.id = vf.version_id - WHERE m.id = ANY($1) OR m.slug = ANY($2) - ), - version_fields_json AS ( - SELECT DISTINCT mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object('version_id', version_id, 'field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value) - ) version_fields_json - FROM version_fields_cte - GROUP BY mod_id - ), - loader_fields_cte AS ( - SELECT DISTINCT vf.mod_id, vf.version_id, lf.*, l.loader - FROM loader_fields lf - INNER JOIN version_fields_cte vf ON lf.id = vf.field_id - LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id - LEFT JOIN loaders l ON lv.loader_id = l.id - GROUP BY vf.mod_id, vf.version_id, lf.enum_type, lf.id, l.loader - ), - loader_fields_json AS ( - SELECT DISTINCT mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'version_id', lf.version_id, - 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional - ) - ) filter (where lf.id is not null) loader_fields_json - FROM loader_fields_cte lf - GROUP BY mod_id - ), - loader_field_enum_values_json AS ( - SELECT DISTINCT mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata - ) - ) filter (where lfev.id is not null) loader_field_enum_values_json - FROM loader_field_enum_values lfev - INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id - GROUP BY mod_id - ), - versions_cte AS ( - SELECT DISTINCT mod_id, v.id as id, date_published - FROM mods m - INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3) - WHERE m.id = ANY($1) OR m.slug = ANY($2) - ), - versions_json AS ( - SELECT DISTINCT mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', id, 'date_published', date_published - ) - ) filter (where id is not null) versions_json - FROM versions_cte - GROUP BY mod_id - ), - loaders_cte AS ( - SELECT DISTINCT mod_id, l.id as id, l.loader - FROM versions_cte - INNER JOIN loaders_versions lv ON versions_cte.id = lv.version_id - INNER JOIN loaders l ON lv.loader_id = l.id - ), - mods_gallery_json AS ( - SELECT DISTINCT mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'image_url', mg.image_url, 'featured', mg.featured, 'name', mg.name, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering - ) - ) filter (where image_url is not null) mods_gallery_json - FROM mods_gallery mg - INNER JOIN mods m ON mg.mod_id = m.id - WHERE m.id = ANY($1) OR m.slug = ANY($2) - GROUP BY mod_id - ), - links_json AS ( - SELECT DISTINCT joining_mod_id as mod_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'platform_id', ml.joining_platform_id, 'platform_name', lp.name,'url', ml.url, 'donation', lp.donation - ) - ) filter (where ml.joining_platform_id is not null) links_json - FROM mods_links ml - INNER JOIN mods m ON ml.joining_mod_id = m.id AND m.id = ANY($1) OR m.slug = ANY($2) - INNER JOIN link_platforms lp ON ml.joining_platform_id = lp.id - GROUP BY mod_id - ) - SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.description description, m.published published, m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status, @@ -654,43 +781,28 @@ impl Project { 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, m.webhook_sent, m.color, 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.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, - v.versions_json versions, - mg.mods_gallery_json gallery, - ml.links_json links, - vf.version_fields_json version_fields, - lf.loader_fields_json loader_fields, - lfev.loader_field_enum_values_json loader_field_enum_values + ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories FROM mods m INNER JOIN threads t ON t.mod_id = m.id - LEFT JOIN mods_gallery_json mg ON mg.mod_id = m.id - LEFT JOIN links_json ml ON ml.mod_id = m.id LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id LEFT JOIN categories c ON mc.joining_category_id = c.id - LEFT JOIN versions_json v ON v.mod_id = m.id - LEFT JOIN loaders_cte l on l.mod_id = m.id - LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id - LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id - LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id - LEFT JOIN games g ON lptg.game_id = g.id - LEFT OUTER JOIN version_fields_json vf ON m.id = vf.mod_id - LEFT OUTER JOIN loader_fields_json lf ON m.id = lf.mod_id - LEFT OUTER JOIN loader_field_enum_values_json lfev ON m.id = lfev.mod_id WHERE m.id = ANY($1) OR m.slug = ANY($2) - GROUP BY t.id, m.id, version_fields_json, loader_fields_json, loader_field_enum_values_json, versions_json, mods_gallery_json, links_json; + GROUP BY t.id, m.id; ", &project_ids_parsed, - &remaining_strings.into_iter().map(|x| x.to_string().to_lowercase()).collect::>(), - &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_listed()).map(|x| x.to_string()).collect::>() + &slugs, ) - .fetch_many(exec) + .fetch_many(&mut *exec) .try_filter_map(|e| async { Ok(e.right().map(|m| { let id = m.id; + let project_id = ProjectId(id); + let (loaders, project_types, games) = loaders_ptypes_games.remove(&project_id).map(|x| x.1).unwrap_or_default(); + let mut versions = versions.remove(&project_id).map(|x| x.1).unwrap_or_default(); + let mut gallery = mods_gallery.remove(&project_id).map(|x| x.1).unwrap_or_default(); + let urls = links.remove(&project_id).map(|x| x.1).unwrap_or_default(); + let version_fields = version_fields.remove(&project_id).map(|x| x.1).unwrap_or_default(); QueryProject { inner: Project { id: ProjectId(id), @@ -722,41 +834,23 @@ impl Project { monetization_status: MonetizationStatus::from_string( &m.monetization_status, ), - loaders: m.loaders.unwrap_or_default(), + loaders, }, categories: m.categories.unwrap_or_default(), additional_categories: m.additional_categories.unwrap_or_default(), - project_types: m.project_types.unwrap_or_default(), - games: m.games.unwrap_or_default(), + project_types, + games, public_versions: { - #[derive(Deserialize)] - struct Version { - pub id: VersionId, - pub date_published: DateTime, - } - - let mut versions: Vec = serde_json::from_value( - m.versions.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - - versions.sort_by(|a, b| a.date_published.cmp(&b.date_published)); - versions.into_iter().map(|x| x.id).collect() + // Each version is a tuple of (VersionId, DateTime) + versions.sort_by(|a, b| a.1.cmp(&b.1)); + versions.into_iter().map(|x| x.0).collect() }, gallery_items: { - let mut gallery: Vec = serde_json::from_value( - m.gallery.unwrap_or_default(), - ).ok().unwrap_or_default(); - gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering)); - gallery }, - urls: serde_json::from_value( - m.links.unwrap_or_default(), - ).unwrap_or_default(), - aggregate_version_fields: VersionField::from_query_json(m.loader_fields, m.version_fields, m.loader_field_enum_values, true), + urls, + aggregate_version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, true), thread_id: ThreadId(m.thread_id), }})) }) diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 799fd027..6f821db8 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -434,7 +434,6 @@ impl User { pub async fn remove( id: UserId, - full: bool, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &RedisPool, ) -> Result, DatabaseError> { @@ -445,38 +444,17 @@ impl User { let deleted_user: UserId = crate::models::users::DELETED_USER.into(); - if full { - let projects: Vec = sqlx::query!( - " - SELECT m.id FROM mods m - INNER JOIN team_members tm ON tm.team_id = m.team_id - WHERE tm.user_id = $1 AND tm.is_owner = TRUE - ", - id as UserId, - ) - .fetch_many(&mut **transaction) - .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) - .try_collect::>() - .await?; - - for project_id in projects { - let _result = - super::project_item::Project::remove(project_id, transaction, redis) - .await?; - } - } else { - sqlx::query!( - " - UPDATE team_members - SET user_id = $1 - WHERE (user_id = $2 AND is_owner = TRUE) - ", - deleted_user as UserId, - id as UserId, - ) - .execute(&mut **transaction) - .await?; - } + sqlx::query!( + " + UPDATE team_members + SET user_id = $1 + WHERE (user_id = $2 AND is_owner = TRUE) + ", + deleted_user as UserId, + id as UserId, + ) + .execute(&mut **transaction) + .await?; sqlx::query!( " diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 7d9bd9e3..c87060e1 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -1,9 +1,13 @@ use super::ids::*; use super::loader_fields::VersionField; use super::DatabaseError; +use crate::database::models::loader_fields::{ + QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, +}; use crate::database::redis::RedisPool; use crate::models::projects::{FileType, VersionStatus}; use chrono::{DateTime, Utc}; +use dashmap::{DashMap, DashSet}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -475,7 +479,7 @@ impl Version { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { Self::get_many(&[id], executor, redis) .await @@ -488,14 +492,21 @@ impl Version { redis: &RedisPool, ) -> Result, DatabaseError> where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + E: sqlx::Acquire<'a, Database = sqlx::Postgres>, { + let version_ids = version_ids + .iter() + .unique() + .copied() + .collect::>(); + use futures::stream::TryStreamExt; if version_ids.is_empty() { return Ok(Vec::new()); } + let mut exec = exec.acquire().await?; let mut redis = redis.connect().await?; let mut version_ids_parsed: Vec = version_ids.iter().map(|x| x.0).collect(); @@ -524,116 +535,231 @@ impl Version { } if !version_ids_parsed.is_empty() { - let db_versions: Vec = sqlx::query!( + let loader_field_ids = DashSet::new(); + let loader_field_enum_value_ids = DashSet::new(); + let version_fields: DashMap> = sqlx::query!( " - WITH version_fields_cte AS ( - SELECT version_id, field_id, int_value, enum_value, string_value - FROM version_fields WHERE version_id = ANY($1) - ), - version_fields_json AS ( - SELECT DISTINCT version_id, - JSONB_AGG( - DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value) - ) version_fields_json - FROM version_fields_cte - GROUP BY version_id - ), - loader_fields_cte AS ( - SELECT DISTINCT vf.version_id, lf.*, l.loader - FROM loader_fields lf - INNER JOIN version_fields_cte vf ON lf.id = vf.field_id - LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id - LEFT JOIN loaders l ON lv.loader_id = l.id - GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader - ), - loader_fields_json AS ( - SELECT DISTINCT version_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'version_id', lf.version_id, - 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional - ) - ) filter (where lf.id is not null) loader_fields_json - FROM loader_fields_cte lf - GROUP BY version_id - ), - loader_field_enum_values_json AS ( - SELECT DISTINCT version_id, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata - ) - ) filter (where lfev.id is not null) loader_field_enum_values_json - FROM loader_field_enum_values lfev - INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id - GROUP BY version_id - ), - files_cte AS ( - SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type - FROM files f - WHERE f.version_id = ANY($1) - ), - files_json AS ( - SELECT DISTINCT version_id, - JSONB_AGG( - DISTINCT jsonb_build_object('id', id, 'url', url, 'filename', filename, 'primary', is_primary, 'size', size, 'file_type', file_type) - ) files_json - FROM files_cte lf - GROUP BY version_id - ), - hashes_json AS ( - SELECT DISTINCT version_id, - JSONB_AGG( - DISTINCT jsonb_build_object('algorithm', algorithm, 'hash', encode(hash, 'escape'), 'file_id', file_id) - ) hashes_json - FROM hashes - INNER JOIN files_cte lf on lf.id = hashes.file_id - GROUP BY version_id - ), - dependencies_json AS ( - SELECT DISTINCT dependent_id as version_id, - 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) - ) dependencies_json - FROM dependencies d - WHERE dependent_id = ANY($1) - GROUP BY version_id - ) + SELECT version_id, field_id, int_value, enum_value, string_value + FROM version_fields + WHERE version_id = ANY($1) + ", + &version_ids_parsed + ) + .fetch(&mut *exec) + .try_fold( + DashMap::new(), + |acc: DashMap>, m| { + let qvf = QueryVersionField { + version_id: VersionId(m.version_id), + field_id: LoaderFieldId(m.field_id), + int_value: m.int_value, + enum_value: m.enum_value.map(LoaderFieldEnumValueId), + string_value: m.string_value, + }; + + loader_field_ids.insert(LoaderFieldId(m.field_id)); + if let Some(enum_value) = m.enum_value { + loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(enum_value)); + } + acc.entry(VersionId(m.version_id)).or_default().push(qvf); + async move { Ok(acc) } + }, + ) + .await?; + + let loader_fields: Vec = sqlx::query!( + " + SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional + FROM loader_fields lf + WHERE id = ANY($1) + ", + &loader_field_ids.iter().map(|x| x.0).collect::>() + ) + .fetch(&mut *exec) + .map_ok(|m| QueryLoaderField { + id: LoaderFieldId(m.id), + field: m.field, + field_type: m.field_type, + enum_type: m.enum_type.map(LoaderFieldEnumId), + min_val: m.min_val, + max_val: m.max_val, + optional: m.optional, + }) + .try_collect() + .await?; + + let loader_field_enum_values: Vec = sqlx::query!( + " + SELECT DISTINCT id, enum_id, value, ordering, created, metadata + FROM loader_field_enum_values lfev + WHERE id = ANY($1) + ORDER BY enum_id, ordering, created ASC + ", + &loader_field_enum_value_ids + .iter() + .map(|x| x.0) + .collect::>() + ) + .fetch(&mut *exec) + .map_ok(|m| QueryLoaderFieldEnumValue { + id: LoaderFieldEnumValueId(m.id), + enum_id: LoaderFieldEnumId(m.enum_id), + value: m.value, + ordering: m.ordering, + created: m.created, + metadata: m.metadata, + }) + .try_collect() + .await?; + + type StringTriple = (Vec, Vec, Vec); + let loaders_ptypes_games: DashMap = sqlx::query!( + " + SELECT DISTINCT version_id, + 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.slug) filter (where g.slug is not null) games + FROM versions v + INNER JOIN loaders_versions lv ON v.id = lv.version_id + INNER JOIN loaders l ON lv.loader_id = l.id + INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id + INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id + INNER JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id + INNER JOIN games g ON lptg.game_id = g.id + WHERE v.id = ANY($1) + GROUP BY version_id + ", + &version_ids_parsed + ).fetch(&mut *exec) + .map_ok(|m| { + let version_id = VersionId(m.version_id); + let loaders = m.loaders.unwrap_or_default(); + let project_types = m.project_types.unwrap_or_default(); + let games = m.games.unwrap_or_default(); + + (version_id, (loaders, project_types, games)) + + } + ).try_collect().await?; + + #[derive(Deserialize)] + struct Hash { + pub file_id: FileId, + pub algorithm: String, + pub hash: String, + } + + #[derive(Deserialize)] + struct File { + pub id: FileId, + pub url: String, + pub filename: String, + pub primary: bool, + pub size: u32, + pub file_type: Option, + } + + let file_ids = DashSet::new(); + let reverse_file_map = DashMap::new(); + let files : DashMap> = sqlx::query!( + " + SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type + FROM files f + WHERE f.version_id = ANY($1) + ", + &version_ids_parsed + ).fetch(&mut *exec) + .try_fold(DashMap::new(), |acc : DashMap>, m| { + let file = File { + id: FileId(m.id), + url: m.url, + filename: m.filename, + primary: m.is_primary, + size: m.size as u32, + file_type: m.file_type.map(|x| FileType::from_string(&x)), + }; + + file_ids.insert(FileId(m.id)); + reverse_file_map.insert(FileId(m.id), VersionId(m.version_id)); + + acc.entry(VersionId(m.version_id)) + .or_default() + .push(file); + async move { Ok(acc) } + } + ).await?; + + let hashes: DashMap> = sqlx::query!( + " + SELECT DISTINCT file_id, algorithm, encode(hash, 'escape') hash + FROM hashes + WHERE file_id = ANY($1) + ", + &file_ids.iter().map(|x| x.0).collect::>() + ) + .fetch(&mut *exec) + .try_fold(DashMap::new(), |acc: DashMap>, m| { + if let Some(found_hash) = m.hash { + let hash = Hash { + file_id: FileId(m.file_id), + algorithm: m.algorithm, + hash: found_hash, + }; + + if let Some(version_id) = reverse_file_map.get(&FileId(m.file_id)) { + acc.entry(*version_id).or_default().push(hash); + } + } + async move { Ok(acc) } + }) + .await?; + + let dependencies : DashMap> = sqlx::query!( + " + SELECT DISTINCT dependent_id as version_id, d.mod_dependency_id as dependency_project_id, d.dependency_id as dependency_version_id, d.dependency_file_name as file_name, d.dependency_type as dependency_type + FROM dependencies d + WHERE dependent_id = ANY($1) + ", + &version_ids_parsed + ).fetch(&mut *exec) + .try_fold(DashMap::new(), |acc : DashMap<_,Vec>, m| { + let dependency = QueryDependency { + project_id: m.dependency_project_id.map(ProjectId), + version_id: m.dependency_version_id.map(VersionId), + file_name: m.file_name, + dependency_type: m.dependency_type, + }; + + acc.entry(VersionId(m.version_id)) + .or_default() + .push(dependency); + async move { Ok(acc) } + } + ).await?; + + let db_versions: Vec = sqlx::query!( + " SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, v.changelog changelog, v.date_published date_published, v.downloads downloads, - 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.slug) filter (where g.slug is not null) games, - f.files_json files, - h.hashes_json hashes, - d.dependencies_json dependencies, - vf.version_fields_json version_fields, - lf.loader_fields_json loader_fields, - lfev.loader_field_enum_values_json loader_field_enum_values + v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering FROM versions v - LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id - LEFT OUTER JOIN loaders l on lv.loader_id = l.id - LEFT OUTER JOIN loaders_project_types lpt on l.id = lpt.joining_loader_id - LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id - LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id - LEFT JOIN games g on lptg.game_id = g.id - LEFT OUTER JOIN files_json f on v.id = f.version_id - LEFT OUTER JOIN hashes_json h on v.id = h.version_id - LEFT OUTER JOIN dependencies_json d on v.id = d.version_id - LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id - LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id - LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id WHERE v.id = ANY($1) - GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, f.files_json, h.hashes_json, d.dependencies_json ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC; ", &version_ids_parsed ) - .fetch_many(exec) + .fetch_many(&mut *exec) .try_filter_map(|e| async { Ok(e.right().map(|v| + { + let version_id = VersionId(v.id); + let (loaders, project_types, games) = loaders_ptypes_games.remove(&version_id).map(|x|x.1).unwrap_or_default(); + let files = files.remove(&version_id).map(|x|x.1).unwrap_or_default(); + let hashes = hashes.remove(&version_id).map(|x|x.1).unwrap_or_default(); + let version_fields = version_fields.remove(&version_id).map(|x|x.1).unwrap_or_default(); + let dependencies = dependencies.remove(&version_id).map(|x|x.1).unwrap_or_default(); + QueryVersion { inner: Version { id: VersionId(v.id), @@ -652,39 +778,10 @@ impl Version { ordering: v.ordering, }, files: { - #[derive(Deserialize)] - struct Hash { - pub file_id: FileId, - pub algorithm: String, - pub hash: String, - } - - #[derive(Deserialize)] - struct File { - pub id: FileId, - pub url: String, - pub filename: String, - pub primary: bool, - pub size: u32, - pub file_type: Option, - } - - let hashes: Vec = serde_json::from_value( - v.hashes.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - - let files: Vec = serde_json::from_value( - v.files.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(); - let mut files = files.into_iter().map(|x| { let mut file_hashes = HashMap::new(); - for hash in &hashes { + for hash in hashes.iter() { if hash.file_id == x.id { file_hashes.insert( hash.algorithm.clone(), @@ -695,8 +792,8 @@ impl Version { QueryFile { id: x.id, - url: x.url, - filename: x.filename, + url: x.url.clone(), + filename: x.filename.clone(), hashes: file_hashes, primary: x.primary, size: x.size, @@ -716,17 +813,13 @@ impl Version { files }, - version_fields: VersionField::from_query_json(v.loader_fields, v.version_fields, v.loader_field_enum_values, false), - loaders: v.loaders.unwrap_or_default(), - project_types: v.project_types.unwrap_or_default(), - games: v.games.unwrap_or_default(), - dependencies: serde_json::from_value( - v.dependencies.unwrap_or_default(), - ) - .ok() - .unwrap_or_default(), + version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, false), + loaders, + project_types, + games, + dependencies, } - )) + })) }) .try_collect::>() .await?; diff --git a/src/models/v2/projects.rs b/src/models/v2/projects.rs index bc6d48c5..b77cc38b 100644 --- a/src/models/v2/projects.rs +++ b/src/models/v2/projects.rs @@ -20,6 +20,7 @@ use crate::routes::{v2_reroute, v3}; use actix_web::web::Data; use actix_web::{web, HttpRequest}; use chrono::{DateTime, Utc}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; @@ -140,20 +141,23 @@ impl LegacyProject { .contains(&Loader("mrpack".to_string())) { project_type = "modpack".to_string(); - if let Some(mrpack_loaders) = versions_item - .fields - .iter() - .find(|f| f.0 == "mrpack_loaders") - { - loaders = mrpack_loaders + if let Some(mrpack_loaders) = data.fields.iter().find(|f| f.0 == "mrpack_loaders") { + let values = mrpack_loaders .1 - .as_array() - .map(|v| { - v.iter() - .filter_map(|l| l.as_str().map(|l| l.to_string())) - .collect::>() - }) - .unwrap_or(Vec::new()); + .iter() + .filter_map(|v| v.as_str()) + .map(|v| v.to_string()) + .collect::>(); + + // drop mrpack from loaders + loaders = loaders + .into_iter() + .filter(|l| l != "mrpack") + .collect::>(); + // and replace with mrpack_loaders + loaders.extend(values); + // remove duplicate loaders + loaders = loaders.into_iter().unique().collect::>(); } } } @@ -381,7 +385,7 @@ impl From for LegacyVersion { // - if loader is mrpack, this is a modpack // the v2 loaders are whatever the corresponding loader fields are let mut loaders = data.loaders.into_iter().map(|l| l.0).collect::>(); - if loaders == vec!["mrpack".to_string()] { + if loaders.contains(&"mrpack".to_string()) { if let Some((_, mrpack_loaders)) = data .fields .into_iter() @@ -464,11 +468,3 @@ impl TryFrom for DonationLink { }) } } - -fn capitalize_first(input: &str) -> String { - let mut result = input.to_owned(); - if let Some(first_char) = result.get_mut(0..1) { - first_char.make_ascii_uppercase(); - } - result -} diff --git a/src/models/v3/collections.rs b/src/models/v3/collections.rs index 77c74d30..52a937bd 100644 --- a/src/models/v3/collections.rs +++ b/src/models/v3/collections.rs @@ -22,7 +22,7 @@ pub struct Collection { /// The title or name of the collection. pub name: String, /// A short description of the collection. - pub description: String, + pub description: Option, /// An icon URL for the collection. pub icon_url: Option, diff --git a/src/queue/payouts.rs b/src/queue/payouts.rs index 1672ec50..29b85263 100644 --- a/src/queue/payouts.rs +++ b/src/queue/payouts.rs @@ -720,6 +720,8 @@ pub async fn process_payout( .execute(&mut *transaction) .await?; + transaction.commit().await?; + if !clear_cache_users.is_empty() { crate::database::models::User::clear_caches( &clear_cache_users @@ -731,8 +733,6 @@ pub async fn process_payout( .await?; } - transaction.commit().await?; - Ok(()) } diff --git a/src/queue/session.rs b/src/queue/session.rs index e99a7507..2f7b3bfd 100644 --- a/src/queue/session.rs +++ b/src/queue/session.rs @@ -131,11 +131,10 @@ impl AuthQueue { .execute(&mut *transaction) .await?; - PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; - update_oauth_access_token_last_used(oauth_access_token_queue, &mut transaction).await?; transaction.commit().await?; + PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; } Ok(()) diff --git a/src/routes/internal/flows.rs b/src/routes/internal/flows.rs index b97e332c..bf831544 100644 --- a/src/routes/internal/flows.rs +++ b/src/routes/internal/flows.rs @@ -1195,8 +1195,8 @@ pub async fn auth_callback( )?; } - crate::database::models::User::clear_caches(&[(id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(id, None)], &redis).await?; if let Some(url) = url { Ok(HttpResponse::TemporaryRedirect() @@ -1395,8 +1395,8 @@ pub async fn delete_auth_provider( } } - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } @@ -1864,8 +1864,8 @@ pub async fn finish_2fa_flow( )?; } - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "backup_codes": codes, @@ -1952,8 +1952,8 @@ pub async fn remove_2fa( )?; } - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } @@ -2138,8 +2138,8 @@ pub async fn change_password( )?; } - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::Ok().finish()) } @@ -2210,8 +2210,8 @@ pub async fn set_email( "We need to verify your email address.", )?; - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::Ok().finish()) } @@ -2300,8 +2300,8 @@ pub async fn verify_email( .await?; Flow::remove(&email.flow, &redis).await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } else { diff --git a/src/routes/internal/mod.rs b/src/routes/internal/mod.rs index 358876c4..81ac4c9b 100644 --- a/src/routes/internal/mod.rs +++ b/src/routes/internal/mod.rs @@ -1,4 +1,4 @@ -mod admin; +pub(crate) mod admin; pub mod flows; pub mod pats; pub mod session; diff --git a/src/routes/internal/pats.rs b/src/routes/internal/pats.rs index b8b2d918..64f349e9 100644 --- a/src/routes/internal/pats.rs +++ b/src/routes/internal/pats.rs @@ -127,12 +127,12 @@ pub async fn create_pat( .insert(&mut transaction) .await?; + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(None, None, Some(user.id.into()))], &redis, ) .await?; - transaction.commit().await?; Ok(HttpResponse::Ok().json(PersonalAccessToken { id: id.into(), @@ -232,12 +232,12 @@ pub async fn edit_pat( .await?; } + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], &redis, ) .await?; - transaction.commit().await?; } } @@ -269,12 +269,12 @@ pub async fn delete_pat( let mut transaction = pool.begin().await?; database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction) .await?; + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], &redis, ) .await?; - transaction.commit().await?; } } diff --git a/src/routes/internal/session.rs b/src/routes/internal/session.rs index 595bc3e3..e2435e48 100644 --- a/src/routes/internal/session.rs +++ b/src/routes/internal/session.rs @@ -187,6 +187,7 @@ pub async fn delete( if session.user_id == current_user.id.into() { let mut transaction = pool.begin().await?; DBSession::remove(session.id, &mut transaction).await?; + transaction.commit().await?; DBSession::clear_cache( vec![( Some(session.id), @@ -196,7 +197,6 @@ pub async fn delete( &redis, ) .await?; - transaction.commit().await?; } } @@ -232,6 +232,7 @@ pub async fn refresh( DBSession::remove(session.id, &mut transaction).await?; let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?; + transaction.commit().await?; DBSession::clear_cache( vec![( Some(session.id), @@ -242,8 +243,6 @@ pub async fn refresh( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::Ok().json(Session::from(new_session, true, None))) } else { Err(ApiError::Authentication( diff --git a/src/routes/updates.rs b/src/routes/updates.rs index 6148f0cb..e562198b 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -76,6 +76,7 @@ pub async fn forge_updates( .collect(), &user_option, &pool, + redis, ) .await?; diff --git a/src/routes/v2/admin.rs b/src/routes/v2/admin.rs deleted file mode 100644 index 07afe836..00000000 --- a/src/routes/v2/admin.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::auth::validate::get_user_record_from_bearer_token; -use crate::database::redis::RedisPool; -use crate::models::analytics::Download; -use crate::models::ids::ProjectId; -use crate::models::pats::Scopes; -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 actix_web::{patch, post, web, HttpRequest, HttpResponse}; -use serde::Deserialize; -use sqlx::PgPool; -use std::collections::HashMap; -use std::net::Ipv4Addr; -use std::sync::Arc; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("admin") - .service(count_download) - .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 { - 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("")) -} - -#[post("/_force_reindex", guard = "admin_key_guard")] -pub async fn force_reindex( - pool: web::Data, - redis: web::Data, - config: web::Data, -) -> Result { - use crate::search::indexing::index_projects; - let redis = redis.get_ref(); - index_projects(pool.as_ref().clone(), redis.clone(), &config).await?; - Ok(HttpResponse::NoContent().finish()) -} diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 417308e6..13f823a6 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -1,4 +1,3 @@ -mod admin; mod moderation; mod notifications; pub(crate) mod project_creation; @@ -20,7 +19,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service( actix_web::web::scope("v2") .wrap(default_cors()) - .configure(admin::config) + .configure(super::internal::admin::config) // Todo: separate these- they need to also follow v2-v3 conversion .configure(super::internal::session::config) .configure(super::internal::flows::config) diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index adb12de1..017b0e10 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -68,16 +68,15 @@ pub async fn project_search( .into_iter() .map(|facets| { facets - .into_iter() - .map(|facet| { - if facet.is_array() { - serde_json::from_value::>(facet).unwrap_or_default() - } else { - vec![serde_json::from_value::(facet.clone()) - .unwrap_or_default()] - } - }) - .collect_vec() + .into_iter() + .map(|facet| { + if facet.is_array() { + serde_json::from_value::>(facet).unwrap_or_default() + } else { + vec![serde_json::from_value::(facet).unwrap_or_default()] + } + }) + .collect_vec() }) .collect_vec(); @@ -212,7 +211,6 @@ pub async fn project_get( session_queue.clone(), ) .await - .or_else(v2_reroute::flatten_404_error) .or_else(v2_reroute::flatten_404_error)?; // Convert response to V2 format @@ -235,7 +233,6 @@ pub async fn project_get_check( v3::projects::project_get_check(info, pool, redis) .await .or_else(v2_reroute::flatten_404_error) - .or_else(v2_reroute::flatten_404_error) } #[derive(Serialize)] @@ -256,7 +253,6 @@ pub async fn dependency_list( v3::projects::dependency_list(req, info, pool, redis, session_queue) .await .or_else(v2_reroute::flatten_404_error) - .or_else(v2_reroute::flatten_404_error) } #[derive(Serialize, Deserialize, Validate)] diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 00ba0726..2713d355 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -4,6 +4,7 @@ use super::ApiError; use crate::database::models::loader_fields::LoaderFieldEnumValue; use crate::database::redis::RedisPool; use crate::models::v2::projects::LegacySideType; +use crate::routes::v2_reroute::capitalize_first; use crate::routes::v3::tags::{ LinkPlatformQueryData, LoaderData as LoaderDataV3, LoaderFieldsEnumQuery, }; @@ -66,7 +67,13 @@ pub async fn loader_list( .map(|l| LoaderData { icon: l.icon, name: l.name, - supported_project_types: l.supported_project_types, + // Add generic 'project' type to all loaders, which is the v2 representation of + // a project type before any versions are set. + supported_project_types: l + .supported_project_types + .into_iter() + .chain(std::iter::once("project".to_string())) + .collect(), }) .collect::>(); Ok(HttpResponse::Ok().json(loaders)) @@ -166,11 +173,12 @@ pub async fn license_text(params: web::Path<(String,)>) -> Result { let platforms = platforms .into_iter() - .map(|p| DonationPlatformQueryData { name: p.name }) + .filter_map(|p| { + if p.donation { + Some(DonationPlatformQueryData { + // Short vs name is no longer a recognized difference in v3. + // We capitalize to recreate the old behavior, with some special handling. + // This may result in different behaviour for platforms added after the v3 migration. + name: match p.name.as_str() { + "bmac" => "Buy Me A Coffee".to_string(), + "github" => "GitHub Sponsors".to_string(), + "ko-fi" => "Ko-fi".to_string(), + "paypal" => "PayPal".to_string(), + // Otherwise, capitalize it + _ => capitalize_first(&p.name), + }, + short: p.name, + }) + } else { + None + } + }) .collect::>(); HttpResponse::Ok().json(platforms) } diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index ecb00acb..7483bef0 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -185,38 +185,17 @@ pub async fn user_icon_edit( .or_else(v2_reroute::flatten_404_error) } -#[derive(Deserialize)] -pub struct RemovalType { - #[serde(default = "default_removal")] - removal_type: String, -} - -fn default_removal() -> String { - "partial".into() -} - #[delete("{id}")] pub async fn user_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, - removal_type: web::Query, redis: web::Data, session_queue: web::Data, ) -> Result { - let removal_type = removal_type.into_inner(); - v3::users::user_delete( - req, - info, - pool, - web::Query(v3::users::RemovalType { - removal_type: removal_type.removal_type, - }), - redis, - session_queue, - ) - .await - .or_else(v2_reroute::flatten_404_error) + v3::users::user_delete(req, info, pool, redis, session_queue) + .await + .or_else(v2_reroute::flatten_404_error) } #[get("{id}/follows")] diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index db0741c0..5faa4afc 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -100,34 +100,66 @@ pub async fn version_create( json!(legacy_create.game_versions), ); + // Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc. + let loaders = match v3::tags::loader_list(client.clone(), redis.clone()).await { + Ok(loader_response) => match v2_reroute::extract_ok_json::< + Vec, + >(loader_response) + .await + { + Ok(loaders) => loaders, + Err(_) => vec![], + }, + Err(_) => vec![], + }; + + let loader_fields_aggregate = loaders + .into_iter() + .filter_map(|loader| { + if legacy_create.loaders.contains(&Loader(loader.name.clone())) { + Some(loader.supported_fields) + } else { + None + } + }) + .flatten() + .collect::>(); + // Copies side types of another version of the project. // If no version exists, defaults to all false. - // TODO: write test for this to ensure predictible unchanging behaviour // This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects, - // so the 'missing' ones can't be easily accessed. + // so the 'missing' ones can't be easily accessed, and versions do need to have these fields explicitly set. let side_type_loader_field_names = [ "singleplayer", "client_and_server", "client_only", "server_only", ]; - fields.extend( - side_type_loader_field_names - .iter() - .map(|f| (f.to_string(), json!(false))), - ); - if let Some(example_version_fields) = - get_example_version_fields(legacy_create.project_id, client, &redis).await? + + // Check if loader_fields_aggregate contains any of these side types + // We assume these four fields are linked together. + if loader_fields_aggregate + .iter() + .any(|f| side_type_loader_field_names.contains(&f.as_str())) { - fields.extend(example_version_fields.into_iter().filter_map(|f| { - if side_type_loader_field_names.contains(&f.field_name.as_str()) { - Some((f.field_name, f.value.serialize_internal())) - } else { - None - } - })); + // If so, we get the fields of the example version of the project, and set the side types to match. + fields.extend( + side_type_loader_field_names + .iter() + .map(|f| (f.to_string(), json!(false))), + ); + if let Some(example_version_fields) = + get_example_version_fields(legacy_create.project_id, client, &redis).await? + { + fields.extend(example_version_fields.into_iter().filter_map(|f| { + if side_type_loader_field_names.contains(&f.field_name.as_str()) { + Some((f.field_name, f.value.serialize_internal())) + } else { + None + } + })); + } } - // Handle project type via file extension prediction let mut project_type = None; for file_part in &legacy_create.file_parts { diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index 34b5d080..492644cc 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -8,7 +8,9 @@ use crate::models::projects::{Dependency, FileType, Version, VersionStatus, Vers use crate::models::v2::projects::LegacyVersion; use crate::queue::session::AuthQueue; use crate::routes::{v2_reroute, v3}; -use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; +use crate::search::SearchConfig; +use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; @@ -22,6 +24,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(version_get) .service(version_delete) .service(version_edit) + .service(version_schedule) .service(super::version_creation::upload_file_to_version), ); } @@ -267,12 +270,45 @@ pub async fn version_edit( #[delete("{version_id}")] pub async fn version_delete( req: HttpRequest, - info: web::Path<(models::ids::VersionId,)>, + info: web::Path<(VersionId,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, + search_config: web::Data, ) -> Result { - v3::versions::version_delete(req, info, pool, redis, session_queue) + v3::versions::version_delete(req, info, pool, redis, session_queue, search_config) .await .or_else(v2_reroute::flatten_404_error) } + +#[derive(Deserialize)] +pub struct SchedulingData { + pub time: DateTime, + pub requested_status: VersionStatus, +} + +#[post("{id}/schedule")] +pub async fn version_schedule( + req: HttpRequest, + info: web::Path<(VersionId,)>, + pool: web::Data, + redis: web::Data, + scheduling_data: web::Json, + session_queue: web::Data, +) -> Result { + let scheduling_data = scheduling_data.into_inner(); + let scheduling_data = v3::versions::SchedulingData { + time: scheduling_data.time, + requested_status: scheduling_data.requested_status, + }; + v3::versions::version_schedule( + req, + info, + pool, + redis, + web::Json(scheduling_data), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error) +} diff --git a/src/routes/v2_reroute.rs b/src/routes/v2_reroute.rs index cd83b0c5..40a0d923 100644 --- a/src/routes/v2_reroute.rs +++ b/src/routes/v2_reroute.rs @@ -300,6 +300,14 @@ pub fn convert_side_types_v2_bools( } } +pub fn capitalize_first(input: &str) -> String { + let mut result = input.to_owned(); + if let Some(first_char) = result.get_mut(0..1) { + first_char.make_ascii_uppercase(); + } + result +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/routes/v3/analytics_get.rs b/src/routes/v3/analytics_get.rs index dc754279..6b81f1a2 100644 --- a/src/routes/v3/analytics_get.rs +++ b/src/routes/v3/analytics_get.rs @@ -413,6 +413,11 @@ pub async fn countries_downloads_get( } } + let hm: HashMap> = hm + .into_iter() + .map(|(key, value)| (key, condense_countries(value))) + .collect(); + Ok(HttpResponse::Ok().json(hm)) } @@ -480,9 +485,31 @@ pub async fn countries_views_get( } } + let hm: HashMap> = hm + .into_iter() + .map(|(key, value)| (key, condense_countries(value))) + .collect(); + Ok(HttpResponse::Ok().json(hm)) } +fn condense_countries(countries: HashMap) -> HashMap { + // Every country under '15' (view or downloads) should be condensed into 'XX' + let mut hm = HashMap::new(); + for (mut country, count) in countries { + if count < 15 { + country = "XX".to_string(); + } + if !hm.contains_key(&country) { + hm.insert(country.to_string(), 0); + } + if let Some(hm) = hm.get_mut(&country) { + *hm += count; + } + } + hm +} + async fn filter_allowed_ids( mut project_ids: Option>, user: crate::models::users::User, diff --git a/src/routes/v3/collections.rs b/src/routes/v3/collections.rs index 0fb4020a..7eff98c5 100644 --- a/src/routes/v3/collections.rs +++ b/src/routes/v3/collections.rs @@ -46,7 +46,7 @@ pub struct CollectionCreateData { pub name: String, #[validate(length(min = 3, max = 255))] /// A short description of the collection. - pub description: String, + pub description: Option, #[validate(length(max = 32))] #[serde(default = "Vec::new")] /// A list of initial projects to use with the created collection @@ -198,7 +198,12 @@ pub struct EditCollection { )] pub name: Option, #[validate(length(min = 3, max = 256))] - pub description: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub description: Option>, pub status: Option, #[validate(length(max = 64))] pub new_projects: Option>, @@ -260,7 +265,7 @@ pub async fn collection_edit( SET description = $1 WHERE (id = $2) ", - description, + description.as_ref(), id as database::models::ids::CollectionId, ) .execute(&mut *transaction) @@ -328,11 +333,22 @@ pub async fn collection_edit( ) .execute(&mut *transaction) .await?; + + sqlx::query!( + " + UPDATE collections + SET updated = NOW() + WHERE id = $1 + ", + collection_item.id as database::models::ids::CollectionId, + ) + .execute(&mut *transaction) + .await?; } + transaction.commit().await?; database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) @@ -417,9 +433,8 @@ pub async fn collection_icon_edit( .execute(&mut *transaction) .await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; + database::models::Collection::clear_cache(collection_item.id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -481,9 +496,8 @@ pub async fn delete_collection_icon( .execute(&mut *transaction) .await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; + database::models::Collection::clear_cache(collection_item.id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -519,9 +533,9 @@ pub async fn collection_delete( let result = database::models::Collection::remove(collection.id, &mut transaction, &redis).await?; - database::models::Collection::clear_cache(collection.id, &redis).await?; transaction.commit().await?; + database::models::Collection::clear_cache(collection.id, &redis).await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index 61400bed..885c7e54 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -456,6 +456,7 @@ pub async fn organizations_edit( .await?; } + transaction.commit().await?; database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -463,7 +464,6 @@ pub async fn organizations_edit( ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -819,6 +819,7 @@ pub async fn organization_icon_edit( .execute(&mut *transaction) .await?; + transaction.commit().await?; database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -826,8 +827,6 @@ pub async fn organization_icon_edit( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -904,6 +903,8 @@ pub async fn delete_organization_icon( .execute(&mut *transaction) .await?; + transaction.commit().await?; + database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -911,7 +912,5 @@ pub async fn delete_organization_icon( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } diff --git a/src/routes/v3/payouts.rs b/src/routes/v3/payouts.rs index 008a8e21..981384da 100644 --- a/src/routes/v3/payouts.rs +++ b/src/routes/v3/payouts.rs @@ -144,12 +144,6 @@ pub async fn paypal_webhook( .execute(&mut *transaction) .await?; - crate::database::models::user_item::User::clear_caches( - &[(crate::database::models::UserId(result.user_id), None)], - &redis, - ) - .await?; - sqlx::query!( " UPDATE payouts @@ -168,6 +162,12 @@ pub async fn paypal_webhook( .await?; transaction.commit().await?; + + crate::database::models::user_item::User::clear_caches( + &[(crate::database::models::UserId(result.user_id), None)], + &redis, + ) + .await?; } } "PAYMENT.PAYOUTS-ITEM.SUCCEEDED" => { @@ -265,12 +265,6 @@ pub async fn tremendous_webhook( .execute(&mut *transaction) .await?; - crate::database::models::user_item::User::clear_caches( - &[(crate::database::models::UserId(result.user_id), None)], - &redis, - ) - .await?; - sqlx::query!( " UPDATE payouts @@ -289,6 +283,12 @@ pub async fn tremendous_webhook( .await?; transaction.commit().await?; + + crate::database::models::user_item::User::clear_caches( + &[(crate::database::models::UserId(result.user_id), None)], + &redis, + ) + .await?; } } "REWARDS.DELIVERY.SUCCEEDED" => { @@ -616,9 +616,9 @@ pub async fn create_payout( .execute(&mut *transaction) .await?; payout_item.insert(&mut transaction).await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } diff --git a/src/routes/v3/projects.rs b/src/routes/v3/projects.rs index 5acf755a..b6dafda6 100644 --- a/src/routes/v3/projects.rs +++ b/src/routes/v3/projects.rs @@ -21,6 +21,7 @@ use crate::models::teams::ProjectPermissions; use crate::models::threads::MessageBody; use crate::queue::session::AuthQueue; use crate::routes::ApiError; +use crate::search::indexing::remove_documents; use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::util::img; use crate::util::routes::read_from_payload; @@ -28,7 +29,7 @@ use crate::util::validate::validation_errors_to_string; use actix_web::{web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; use futures::TryStreamExt; -use meilisearch_sdk::indexes::IndexesResults; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; @@ -237,7 +238,7 @@ pub async fn project_edit( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, - config: web::Data, + search_config: web::Data, new_project: web::Json, redis: web::Data, session_queue: web::Data, @@ -482,7 +483,15 @@ pub async fn project_edit( .await?; if project_item.inner.status.is_searchable() && !status.is_searchable() { - delete_from_index(id.into(), config).await?; + remove_documents( + &project_item + .versions + .into_iter() + .map(|x| x.into()) + .collect::>(), + &search_config, + ) + .await?; } } @@ -840,6 +849,8 @@ pub async fn project_edit( }; img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -848,7 +859,6 @@ pub async fn project_edit( ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -920,21 +930,6 @@ pub async fn project_search( Ok(HttpResponse::Ok().json(results)) } -pub async fn delete_from_index( - id: ProjectId, - config: web::Data, -) -> Result<(), meilisearch_sdk::errors::Error> { - let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key); - - let indexes: IndexesResults = client.get_indexes().await?; - - for index in indexes.results { - index.delete_document(id.to_string()).await?; - } - - Ok(()) -} - //checks the validity of a project id or slug pub async fn project_get_check( info: web::Path<(String,)>, @@ -989,7 +984,6 @@ pub async fn dependency_list( let dependencies = database::Project::get_dependencies(project.inner.id, &**pool, &redis).await?; - let project_ids = dependencies .iter() .filter_map(|x| { @@ -1003,11 +997,13 @@ pub async fn dependency_list( x.1 } }) + .unique() .collect::>(); let dep_version_ids = dependencies .iter() .filter_map(|x| x.0) + .unique() .collect::>(); let (projects_result, versions_result) = futures::future::try_join( database::Project::get_many_ids(&project_ids, &**pool, &redis), @@ -1516,6 +1512,7 @@ pub async fn project_icon_edit( .execute(&mut *transaction) .await?; + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -1524,8 +1521,6 @@ pub async fn project_icon_edit( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -1611,11 +1606,10 @@ pub async fn delete_project_icon( .execute(&mut *transaction) .await?; + transaction.commit().await?; db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } @@ -1751,6 +1745,7 @@ pub async fn add_gallery_item( }]; GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?; + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -1759,8 +1754,6 @@ pub async fn add_gallery_item( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -1936,11 +1929,11 @@ pub async fn edit_gallery_item( .await?; } + transaction.commit().await?; + db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } @@ -2042,11 +2035,11 @@ pub async fn delete_gallery_item( .execute(&mut *transaction) .await?; + transaction.commit().await?; + db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } @@ -2055,7 +2048,7 @@ pub async fn project_delete( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - config: web::Data, + search_config: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( @@ -2128,7 +2121,15 @@ pub async fn project_delete( transaction.commit().await?; - delete_from_index(project.inner.id.into(), config).await?; + remove_documents( + &project + .versions + .into_iter() + .map(|x| x.into()) + .collect::>(), + &search_config, + ) + .await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index 8d4cfdf6..352b3eb0 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -217,6 +217,7 @@ pub async fn license_text(params: web::Path<(String,)>) -> Result = LinkPlatform::list(&**pool, &redis) .await? .into_iter() - .map(|x| LinkPlatformQueryData { name: x.name }) + .map(|x| LinkPlatformQueryData { + name: x.name, + donation: x.donation, + }) .collect(); Ok(HttpResponse::Ok().json(results)) } diff --git a/src/routes/v3/teams.rs b/src/routes/v3/teams.rs index 81c4d601..338d97b1 100644 --- a/src/routes/v3/teams.rs +++ b/src/routes/v3/teams.rs @@ -348,10 +348,10 @@ pub async fn join_team( ) .await?; + transaction.commit().await?; + User::clear_project_cache(&[current_user.id.into()], &redis).await?; TeamMember::clear_cache(team_id, &redis).await?; - - transaction.commit().await?; } else { return Err(ApiError::InvalidInput( "There is no pending request from this team".to_string(), @@ -542,9 +542,8 @@ pub async fn add_team_member( } } - TeamMember::clear_cache(team_id, &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(team_id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -691,9 +690,8 @@ pub async fn edit_team_member( ) .await?; - TeamMember::clear_cache(id, &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -797,9 +795,8 @@ pub async fn transfer_ownership( ) .await?; - TeamMember::clear_cache(id.into(), &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(id.into(), &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -925,10 +922,11 @@ pub async fn remove_team_member( } } + transaction.commit().await?; + TeamMember::clear_cache(id, &redis).await?; User::clear_project_cache(&[delete_member.user_id], &redis).await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 7d14279c..f997dd70 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -449,8 +449,8 @@ pub async fn user_edit( .await?; } - User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; transaction.commit().await?; + User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -546,21 +546,10 @@ pub async fn user_icon_edit( } } -#[derive(Deserialize)] -pub struct RemovalType { - #[serde(default = "default_removal")] - pub removal_type: String, -} - -fn default_removal() -> String { - "partial".into() -} - pub async fn user_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, - removal_type: web::Query, redis: web::Data, session_queue: web::Data, ) -> Result { @@ -584,13 +573,7 @@ pub async fn user_delete( let mut transaction = pool.begin().await?; - let result = User::remove( - id, - removal_type.removal_type == "full", - &mut transaction, - &redis, - ) - .await?; + let result = User::remove(id, &mut transaction, &redis).await?; transaction.commit().await?; diff --git a/src/routes/v3/version_file.rs b/src/routes/v3/version_file.rs index 22bdb577..d6abfbf9 100644 --- a/src/routes/v3/version_file.rs +++ b/src/routes/v3/version_file.rs @@ -238,6 +238,7 @@ pub async fn get_versions_from_hashes( database::models::Version::get_many(&version_ids, &**pool, &redis).await?, &user_option, &pool, + redis, ) .await?; diff --git a/src/routes/v3/versions.rs b/src/routes/v3/versions.rs index 2df9fd00..ed5561ba 100644 --- a/src/routes/v3/versions.rs +++ b/src/routes/v3/versions.rs @@ -21,6 +21,8 @@ use crate::models::projects::{skip_nulls, Loader, Version}; use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType}; use crate::models::teams::ProjectPermissions; use crate::queue::session::AuthQueue; +use crate::search::indexing::remove_documents; +use crate::search::SearchConfig; use crate::util::img; use crate::util::validate::validation_errors_to_string; use actix_web::{web, HttpRequest, HttpResponse}; @@ -135,7 +137,7 @@ pub async fn versions_get( .map(|x| x.1) .ok(); - let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?; + let versions = filter_authorized_versions(versions_data, &user_option, &pool, redis).await?; Ok(HttpResponse::Ok().json(versions)) } @@ -665,6 +667,7 @@ pub async fn version_edit_helper( img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + transaction.commit().await?; database::models::Version::clear_cache(&version_item, &redis).await?; database::models::Project::clear_cache( version_item.inner.project_id, @@ -673,7 +676,6 @@ pub async fn version_edit_helper( &redis, ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -889,7 +891,7 @@ pub async fn version_list_inner( response.sort(); response.dedup_by(|a, b| a.inner.id == b.inner.id); - let response = filter_authorized_versions(response, &user_option, &pool).await?; + let response = filter_authorized_versions(response, &user_option, &pool, redis).await?; Ok(response) } else { @@ -990,8 +992,8 @@ pub async fn version_schedule( .execute(&mut *transaction) .await?; - database::models::Version::clear_cache(&version_item, &redis).await?; transaction.commit().await?; + database::models::Version::clear_cache(&version_item, &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1005,6 +1007,7 @@ pub async fn version_delete( pool: web::Data, redis: web::Data, session_queue: web::Data, + search_config: web::Data, ) -> Result { let user = get_user_from_headers( &req, @@ -1072,12 +1075,11 @@ pub async fn version_delete( let result = database::models::Version::remove_full(version.inner.id, &redis, &mut transaction).await?; - + transaction.commit().await?; + remove_documents(&[version.inner.id.into()], &search_config).await?; database::models::Project::clear_cache(version.inner.project_id, None, Some(true), &redis) .await?; - transaction.commit().await?; - if result.is_some() { Ok(HttpResponse::NoContent().body("")) } else { diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 4c4d478e..319d5d75 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -12,14 +12,10 @@ use crate::models; use crate::search::UploadSearchProject; use sqlx::postgres::PgPool; -pub async fn index_local( +pub async fn get_all_ids( pool: PgPool, - redis: &RedisPool, -) -> Result<(Vec, Vec), IndexingError> { - info!("Indexing local projects!"); - let loader_field_keys: Arc> = Arc::new(DashSet::new()); - - let all_visible_ids: HashMap = sqlx::query!( +) -> Result, IndexingError> { + let all_visible_ids: Vec<(VersionId, ProjectId, String)> = sqlx::query!( " SELECT v.id id, m.id mod_id, u.username owner_username @@ -45,33 +41,48 @@ pub async fn index_local( Ok(e.right().map(|m| { let project_id: ProjectId = ProjectId(m.mod_id); let version_id: VersionId = VersionId(m.id); - (version_id, (project_id, m.owner_username)) + (version_id, project_id, m.owner_username) })) }) - .try_collect::>() + .try_collect::>() .await?; - let project_ids = all_visible_ids + Ok(all_visible_ids) +} + +pub async fn index_local( + pool: &PgPool, + redis: &RedisPool, + visible_ids: HashMap, +) -> Result<(Vec, Vec), IndexingError> { + info!("Indexing local projects!"); + let loader_field_keys: Arc> = Arc::new(DashSet::new()); + + let project_ids = visible_ids .values() .map(|(project_id, _)| project_id) .cloned() .collect::>(); - let projects: HashMap<_, _> = project_item::Project::get_many_ids(&project_ids, &pool, redis) + let projects: HashMap<_, _> = project_item::Project::get_many_ids(&project_ids, pool, redis) .await? .into_iter() .map(|p| (p.inner.id, p)) .collect(); - let version_ids = all_visible_ids.keys().cloned().collect::>(); - let versions: HashMap<_, _> = version_item::Version::get_many(&version_ids, &pool, redis) + info!("Fetched local projects!"); + + let version_ids = visible_ids.keys().cloned().collect::>(); + let versions: HashMap<_, _> = version_item::Version::get_many(&version_ids, pool, redis) .await? .into_iter() .map(|v| (v.inner.id, v)) .collect(); + info!("Fetched local versions!"); + let mut uploads = Vec::new(); // TODO: could possibly clone less here? - for (version_id, (project_id, owner_username)) in all_visible_ids { + for (version_id, (project_id, owner_username)) in visible_ids { let m = projects.get(&project_id); let v = versions.get(&version_id); diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index 7fbef6e4..c7790301 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -1,15 +1,22 @@ /// This module is used for the indexing from any source. pub mod local_import; +use itertools::Itertools; +use std::collections::HashMap; + use crate::database::redis::RedisPool; +use crate::models::ids::base62_impl::to_base62; use crate::search::{SearchConfig, UploadSearchProject}; use local_import::index_local; +use log::info; use meilisearch_sdk::client::Client; use meilisearch_sdk::indexes::Index; use meilisearch_sdk::settings::{PaginationSetting, Settings}; use sqlx::postgres::PgPool; use thiserror::Error; +use self::local_import::get_all_ids; + #[derive(Error, Debug)] pub enum IndexingError { #[error("Error while connecting to the MeiliSearch database")] @@ -28,60 +35,214 @@ pub enum IndexingError { // The chunk size for adding projects to the indexing database. If the request size // is too large (>10MiB) then the request fails with an error. This chunk size -// assumes a max average size of 1KiB per project to avoid this cap. -const MEILISEARCH_CHUNK_SIZE: usize = 10000; +// assumes a max average size of 4KiB per project to avoid this cap. +const MEILISEARCH_CHUNK_SIZE: usize = 2500; // Should be less than FETCH_PROJECT_SIZE +const FETCH_PROJECT_SIZE: usize = 5000; + +const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); + +pub async fn remove_documents( + ids: &[crate::models::ids::VersionId], + config: &SearchConfig, +) -> Result<(), meilisearch_sdk::errors::Error> { + let indexes = get_indexes(config).await?; + + for index in indexes { + index + .delete_documents(&ids.iter().map(|x| to_base62(x.0)).collect::>()) + .await?; + } + + Ok(()) +} pub async fn index_projects( pool: PgPool, redis: RedisPool, config: &SearchConfig, ) -> Result<(), IndexingError> { - let mut docs_to_add: Vec = vec![]; - let mut additional_fields: Vec = vec![]; + info!("Indexing projects."); + + let indices = get_indexes(config).await?; + + let all_ids = get_all_ids(pool.clone()).await?; + let all_ids_len = all_ids.len(); + info!("Got all ids, indexing {} projects", all_ids_len); + + let mut so_far = 0; + let as_chunks: Vec<_> = all_ids + .into_iter() + .chunks(FETCH_PROJECT_SIZE) + .into_iter() + .map(|x| x.collect::>()) + .collect(); - let (mut uploads, mut loader_fields) = index_local(pool.clone(), &redis).await?; - docs_to_add.append(&mut uploads); - additional_fields.append(&mut loader_fields); + for id_chunk in as_chunks { + info!( + "Fetching chunk {}-{}/{}, size: {}", + so_far, + so_far + FETCH_PROJECT_SIZE, + all_ids_len, + id_chunk.len() + ); + so_far += FETCH_PROJECT_SIZE; - // Write Indices - add_projects(docs_to_add, additional_fields, config).await?; + let id_chunk = id_chunk + .into_iter() + .map(|(version_id, project_id, owner_username)| { + (version_id, (project_id, owner_username.to_lowercase())) + }) + .collect::>(); + let (uploads, loader_fields) = index_local(&pool, &redis, id_chunk).await?; + info!("Got chunk, adding to docs_to_add"); + add_projects(&indices, uploads, loader_fields, config).await?; + } + + info!("Done adding projects."); Ok(()) } -async fn create_index( +pub async fn get_indexes( + config: &SearchConfig, +) -> Result, meilisearch_sdk::errors::Error> { + let client = config.make_client(); + + let projects_index = create_or_update_index(&client, "projects", None).await?; + let projects_filtered_index = create_or_update_index( + &client, + "projects_filtered", + Some(&[ + "sort", + "words", + "typo", + "proximity", + "attribute", + "exactness", + ]), + ) + .await?; + + Ok(vec![projects_index, projects_filtered_index]) +} + +async fn create_or_update_index( client: &Client, name: &'static str, custom_rules: Option<&'static [&'static str]>, -) -> Result { - client - .delete_index(name) - .await? - .wait_for_completion(client, None, None) - .await?; +) -> Result { + info!("Updating/creating index."); match client.get_index(name).await { Ok(index) => { - index - .set_settings(&default_settings()) - .await? - .wait_for_completion(client, None, None) - .await?; + info!("Updating index settings."); + + let old_settings = index.get_settings().await?; + + let mut settings = default_settings(); + + if let Some(custom_rules) = custom_rules { + settings = settings.with_ranking_rules(custom_rules); + } + + let old_settings = Settings { + synonyms: None, // We don't use synonyms right now + stop_words: if settings.stop_words.is_none() { + None + } else { + old_settings.stop_words.map(|mut x| { + x.sort(); + x + }) + }, + ranking_rules: if settings.ranking_rules.is_none() { + None + } else { + old_settings.ranking_rules + }, + filterable_attributes: if settings.filterable_attributes.is_none() { + None + } else { + old_settings.filterable_attributes.map(|mut x| { + x.sort(); + x + }) + }, + sortable_attributes: if settings.sortable_attributes.is_none() { + None + } else { + old_settings.sortable_attributes.map(|mut x| { + x.sort(); + x + }) + }, + distinct_attribute: if settings.distinct_attribute.is_none() { + None + } else { + old_settings.distinct_attribute + }, + searchable_attributes: if settings.searchable_attributes.is_none() { + None + } else { + old_settings.searchable_attributes + }, + displayed_attributes: if settings.displayed_attributes.is_none() { + None + } else { + old_settings.displayed_attributes.map(|mut x| { + x.sort(); + x + }) + }, + pagination: if settings.pagination.is_none() { + None + } else { + old_settings.pagination + }, + faceting: if settings.faceting.is_none() { + None + } else { + old_settings.faceting + }, + typo_tolerance: None, // We don't use typo tolerance right now + dictionary: None, // We don't use dictionary right now + }; + + if old_settings.synonyms != settings.synonyms + || old_settings.stop_words != settings.stop_words + || old_settings.ranking_rules != settings.ranking_rules + || old_settings.filterable_attributes != settings.filterable_attributes + || old_settings.sortable_attributes != settings.sortable_attributes + || old_settings.distinct_attribute != settings.distinct_attribute + || old_settings.searchable_attributes != settings.searchable_attributes + || old_settings.displayed_attributes != settings.displayed_attributes + || old_settings.pagination != settings.pagination + || old_settings.faceting != settings.faceting + || old_settings.typo_tolerance != settings.typo_tolerance + || old_settings.dictionary != settings.dictionary + { + info!("Performing index settings set."); + index + .set_settings(&settings) + .await? + .wait_for_completion(client, None, Some(TIMEOUT)) + .await?; + info!("Done performing index settings set."); + } Ok(index) } - Err(meilisearch_sdk::errors::Error::Meilisearch( - meilisearch_sdk::errors::MeilisearchError { - error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound, - .. - }, - )) => { + _ => { + info!("Creating index."); + // Only create index and set settings if the index doesn't already exist let task = client.create_index(name, Some("version_id")).await?; - let task = task.wait_for_completion(client, None, None).await?; + let task = task + .wait_for_completion(client, None, Some(TIMEOUT)) + .await?; let index = task .try_make_index(client) - .map_err(|_| IndexingError::Task)?; + .map_err(|x| x.unwrap_failure())?; let mut settings = default_settings(); @@ -92,47 +253,47 @@ async fn create_index( index .set_settings(&settings) .await? - .wait_for_completion(client, None, None) + .wait_for_completion(client, None, Some(TIMEOUT)) .await?; Ok(index) } - Err(e) => { - log::warn!("Unhandled error while creating index: {}", e); - Err(IndexingError::Indexing(e)) - } } } async fn add_to_index( client: &Client, - index: Index, + index: &Index, mods: &[UploadSearchProject], ) -> Result<(), IndexingError> { for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) { + info!( + "Adding chunk starting with version id {}", + chunk[0].version_id + ); index - .add_documents(chunk, Some("version_id")) + .add_or_replace(chunk, Some("version_id")) .await? - .wait_for_completion(client, None, None) + .wait_for_completion(client, None, Some(std::time::Duration::from_secs(3600))) .await?; + info!("Added chunk of {} projects to index", chunk.len()); } + Ok(()) } -async fn create_and_add_to_index( +async fn update_and_add_to_index( client: &Client, + index: &Index, projects: &[UploadSearchProject], additional_fields: &[String], - name: &'static str, - custom_rules: Option<&'static [&'static str]>, ) -> Result<(), IndexingError> { - let index = create_index(client, name, custom_rules).await?; - - let mut new_filterable_attributes = index.get_filterable_attributes().await?; + let mut new_filterable_attributes: Vec = index.get_filterable_attributes().await?; let mut new_displayed_attributes = index.get_displayed_attributes().await?; new_filterable_attributes.extend(additional_fields.iter().map(|s| s.to_string())); new_displayed_attributes.extend(additional_fields.iter().map(|s| s.to_string())); + info!("add attributes."); index .set_filterable_attributes(new_filterable_attributes) .await?; @@ -140,45 +301,41 @@ async fn create_and_add_to_index( .set_displayed_attributes(new_displayed_attributes) .await?; + info!("Adding to index."); + add_to_index(client, index, projects).await?; Ok(()) } pub async fn add_projects( + indices: &[Index], projects: Vec, additional_fields: Vec, config: &SearchConfig, ) -> Result<(), IndexingError> { let client = config.make_client(); - - create_and_add_to_index(&client, &projects, &additional_fields, "projects", None).await?; - - create_and_add_to_index( - &client, - &projects, - &additional_fields, - "projects_filtered", - Some(&[ - "sort", - "words", - "typo", - "proximity", - "attribute", - "exactness", - ]), - ) - .await?; + for index in indices { + info!("adding projects part1 or 2."); + update_and_add_to_index(&client, index, &projects, &additional_fields).await?; + } Ok(()) } fn default_settings() -> Settings { + let mut sorted_display = DEFAULT_DISPLAYED_ATTRIBUTES.to_vec(); + sorted_display.sort(); + let mut sorted_sortable = DEFAULT_SORTABLE_ATTRIBUTES.to_vec(); + sorted_sortable.sort(); + let mut sorted_attrs = DEFAULT_ATTRIBUTES_FOR_FACETING.to_vec(); + sorted_attrs.sort(); + Settings::new() .with_distinct_attribute("project_id") - .with_displayed_attributes(DEFAULT_DISPLAYED_ATTRIBUTES) + .with_displayed_attributes(sorted_display) .with_searchable_attributes(DEFAULT_SEARCHABLE_ATTRIBUTES) - .with_sortable_attributes(DEFAULT_SORTABLE_ATTRIBUTES) - .with_filterable_attributes(DEFAULT_ATTRIBUTES_FOR_FACETING) + .with_sortable_attributes(sorted_sortable) + .with_filterable_attributes(sorted_attrs) .with_pagination(PaginationSetting { max_total_hits: 2147483647, }) diff --git a/src/search/mod.rs b/src/search/mod.rs index 83160325..6b9e160c 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -67,7 +67,7 @@ pub struct SearchConfig { impl SearchConfig { pub fn make_client(&self) -> Client { - Client::new(self.address.as_str(), self.key.as_str()) + Client::new(self.address.as_str(), Some(self.key.as_str())) } } @@ -164,8 +164,8 @@ pub struct ResultSearchProject { pub requested_status: Option, pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. pub links: Vec, - pub games: Vec, // Todo: in future, could be a searchable field. pub gallery_items: Vec, // Gallery *only* urls are stored in gallery, but the gallery items are stored here- required for the Project model. + pub games: Vec, // Todo: in future, could be a searchable field. pub organization_id: Option, // Todo: in future, could be a searchable field. #[serde(flatten)] @@ -187,7 +187,7 @@ pub async fn search_for_project( info: &SearchRequest, config: &SearchConfig, ) -> Result { - let client = Client::new(&*config.address, &*config.key); + let client = Client::new(&*config.address, Some(&*config.key)); let offset = info.offset.as_deref().unwrap_or("0").parse()?; let index = info.index.as_deref().unwrap_or("relevance"); @@ -237,7 +237,7 @@ pub async fn search_for_project( if facet.is_array() { serde_json::from_value::>(facet).unwrap_or_default() } else { - vec![serde_json::from_value::(facet.clone()) + vec![serde_json::from_value::(facet) .unwrap_or_default()] } }) diff --git a/src/util/webhook.rs b/src/util/webhook.rs index 06370933..380b95e6 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -1,5 +1,4 @@ use crate::database::models::legacy_loader_fields::MinecraftGameVersion; -use crate::database::models::loader_fields::VersionField; use crate::database::redis::RedisPool; use crate::models::projects::ProjectId; use crate::routes::ApiError; @@ -80,12 +79,14 @@ pub async fn send_discord_webhook( ) -> Result<(), ApiError> { // TODO: this currently uses Minecraft as it is a v2 webhook, and requires 'game_versions', a minecraft-java loader field. // TODO: This should be updated to use the generic loader fields w/ discord from the project game + + // TODO: This should use the project_item get route let all_game_versions = MinecraftGameVersion::list(pool, redis).await?; let row = sqlx::query!( " - SELECT m.id id, m.name name, m.description description, m.color color, + SELECT m.id id, m.name name, m.summary summary, m.color color, m.icon_url icon_url, m.slug slug, u.username username, u.avatar_url avatar_url, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories, @@ -93,38 +94,7 @@ pub async fn send_discord_webhook( ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, 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( - DISTINCT jsonb_build_object( - 'field_id', vf.field_id, - 'int_value', vf.int_value, - 'enum_value', vf.enum_value, - 'string_value', vf.string_value - ) - ) filter (where vf.field_id is not null) version_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'version_id', 0, -- TODO: When webhook is updated to match others, this should match version - 'lf_id', lf.id, - 'loader_name', lo.loader, - 'field', lf.field, - 'field_type', lf.field_type, - 'enum_type', lf.enum_type, - 'min_val', lf.min_val, - 'max_val', lf.max_val, - 'optional', lf.optional - ) - ) filter (where lf.id is not null) loader_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', lfev.id, - 'enum_id', lfev.enum_id, - 'value', lfev.value, - 'ordering', lfev.ordering, - 'created', lfev.created, - 'metadata', lfev.metadata - ) - ) filter (where lfev.id is not null) loader_field_enum_values + ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id @@ -138,10 +108,6 @@ pub async fn send_discord_webhook( LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE INNER JOIN users u ON tm.user_id = u.id - LEFT OUTER JOIN version_fields vf on v.id = vf.version_id - LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id - 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, u.id; ", @@ -157,11 +123,6 @@ pub async fn send_discord_webhook( let categories = project.categories.unwrap_or_default(); let loaders = project.loaders.unwrap_or_default(); - // let versions: Vec = - // serde_json::from_value(project.versions.unwrap_or_default()) - // .ok() - // .unwrap_or_default(); - if !categories.is_empty() { fields.push(DiscordEmbedField { name: "Categories", @@ -226,12 +187,17 @@ pub async fn send_discord_webhook( // TODO: Modified to keep "Versions" as a field as it may be hardcoded. Ideally, this pushes all loader fields to the embed for v3 // TODO: This might need some work to manually test - let version_fields = VersionField::from_query_json( - project.loader_fields, - project.version_fields, - project.loader_field_enum_values, - true, - ); + let version_fields = crate::database::models::project_item::Project::get_id( + crate::database::models::ids::ProjectId(project.id), + pool, + redis, + ) + .await + .ok() + .flatten() + .map(|project| project.aggregate_version_fields) + .unwrap_or_default(); + let versions = version_fields .into_iter() .find_map(|vf| MinecraftGameVersion::try_from_version_field(&vf).ok()) @@ -279,7 +245,7 @@ pub async fn send_discord_webhook( project.slug.unwrap_or_else(|| project_id.to_string()) ), title: project.name, // Do not change DiscordEmbed - description: project.description, + description: project.summary, timestamp: Utc::now(), color: project.color.unwrap_or(0x1bd96a) as u32, fields, diff --git a/tests/analytics.rs b/tests/analytics.rs index b83d7989..7318daa5 100644 --- a/tests/analytics.rs +++ b/tests/analytics.rs @@ -1,4 +1,3 @@ -use actix_web::test; use chrono::{DateTime, Duration, Utc}; use common::permissions::PermissionsTest; use common::permissions::PermissionsTestContext; @@ -166,15 +165,23 @@ pub async fn permissions_analytics_revenue() { .team_id .clone(); + let api = &test_env.api; + 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", - )) + let req_gen = |ctx: PermissionsTestContext| async move { + let project_id = ctx.project_id.unwrap(); + let ids_or_slugs = vec![project_id.as_str()]; + api.get_analytics_revenue( + ids_or_slugs, + false, + None, + None, + Some(5), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) @@ -197,12 +204,20 @@ pub async fn permissions_analytics_revenue() { // 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", - )) + let req_gen = |ctx: PermissionsTestContext| { + let alpha_version_id = alpha_version_id.clone(); + async move { + let ids_or_slugs = vec![alpha_version_id.as_str()]; + api.get_analytics_revenue( + ids_or_slugs, + true, + None, + None, + Some(5), + ctx.test_pat.as_deref(), + ) + .await + } }; PermissionsTest::new(&test_env) diff --git a/tests/common/api_common/generic.rs b/tests/common/api_common/generic.rs index 1e991898..84f0d960 100644 --- a/tests/common/api_common/generic.rs +++ b/tests/common/api_common/generic.rs @@ -10,8 +10,8 @@ use labrinth::models::{ use crate::common::{api_v2::ApiV2, api_v3::ApiV3, dummy_data::TestFile}; use super::{ - models::{CommonImageData, CommonProject, CommonVersion}, - request_data::ProjectCreationRequestData, + models::{CommonProject, CommonVersion}, + request_data::{ImageData, ProjectCreationRequestData}, Api, ApiProject, ApiTags, ApiTeams, ApiVersion, }; @@ -65,17 +65,23 @@ impl Api for GenericApi { delegate_api_variant!( #[async_trait(?Send)] impl ApiProject for GenericApi { - [add_public_project, (CommonProject, Vec), slug: &str, version_jar: Option, modify_json: Option, pat: &str], + [add_public_project, (CommonProject, Vec), slug: &str, version_jar: Option, modify_json: Option, pat: Option<&str>], [get_public_project_creation_data_json, serde_json::Value, slug: &str, version_jar: Option<&TestFile>], - [create_project, ServiceResponse, creation_data: ProjectCreationRequestData, pat: &str], - [remove_project, ServiceResponse, project_slug_or_id: &str, pat: &str], - [get_project, ServiceResponse, id_or_slug: &str, pat: &str], - [get_project_deserialized_common, CommonProject, id_or_slug: &str, pat: &str], - [get_user_projects, ServiceResponse, user_id_or_username: &str, pat: &str], - [get_user_projects_deserialized_common, Vec, user_id_or_username: &str, pat: &str], - [edit_project, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: &str], - [edit_project_bulk, ServiceResponse, ids_or_slugs: &[&str], patch: serde_json::Value, pat: &str], - [edit_project_icon, ServiceResponse, id_or_slug: &str, icon: Option, pat: &str], + [create_project, ServiceResponse, creation_data: ProjectCreationRequestData, pat: Option<&str>], + [remove_project, ServiceResponse, project_slug_or_id: &str, pat: Option<&str>], + [get_project, ServiceResponse, id_or_slug: &str, pat: Option<&str>], + [get_project_deserialized_common, CommonProject, id_or_slug: &str, pat: Option<&str>], + [get_user_projects, ServiceResponse, user_id_or_username: &str, pat: Option<&str>], + [get_user_projects_deserialized_common, Vec, user_id_or_username: &str, pat: Option<&str>], + [edit_project, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: Option<&str>], + [edit_project_bulk, ServiceResponse, ids_or_slugs: &[&str], patch: serde_json::Value, pat: Option<&str>], + [edit_project_icon, ServiceResponse, id_or_slug: &str, icon: Option, pat: Option<&str>], + [schedule_project, ServiceResponse, id_or_slug: &str, requested_status: &str, date : chrono::DateTime, pat: Option<&str>], + [add_gallery_item, ServiceResponse, id_or_slug: &str, image: ImageData, featured: bool, title: Option, description: Option, ordering: Option, pat: Option<&str>], + [remove_gallery_item, ServiceResponse, id_or_slug: &str, image_url: &str, pat: Option<&str>], + [edit_gallery_item, ServiceResponse, id_or_slug: &str, image_url: &str, patch: HashMap, pat: Option<&str>], + [create_report, ServiceResponse, report_type: &str, id: &str, item_type: crate::common::api_common::models::CommonItemType, body: &str, pat: Option<&str>], + [get_report, ServiceResponse, id: &str, pat: Option<&str>], } ); @@ -92,44 +98,47 @@ delegate_api_variant!( delegate_api_variant!( #[async_trait(?Send)] impl ApiTeams for GenericApi { - [get_team_members, ServiceResponse, team_id: &str, pat: &str], - [get_team_members_deserialized_common, Vec, team_id: &str, pat: &str], - [get_project_members, ServiceResponse, id_or_slug: &str, pat: &str], - [get_project_members_deserialized_common, Vec, id_or_slug: &str, pat: &str], - [get_organization_members, ServiceResponse, id_or_title: &str, pat: &str], - [get_organization_members_deserialized_common, Vec, id_or_title: &str, pat: &str], - [join_team, ServiceResponse, team_id: &str, pat: &str], - [remove_from_team, ServiceResponse, team_id: &str, user_id: &str, pat: &str], - [edit_team_member, ServiceResponse, team_id: &str, user_id: &str, patch: serde_json::Value, pat: &str], - [transfer_team_ownership, ServiceResponse, team_id: &str, user_id: &str, pat: &str], - [get_user_notifications, ServiceResponse, user_id: &str, pat: &str], - [get_user_notifications_deserialized_common, Vec, user_id: &str, pat: &str], - [mark_notification_read, ServiceResponse, notification_id: &str, pat: &str], - [add_user_to_team, ServiceResponse, team_id: &str, user_id: &str, project_permissions: Option, organization_permissions: Option, pat: &str], - [delete_notification, ServiceResponse, notification_id: &str, pat: &str], + [get_team_members, ServiceResponse, team_id: &str, pat: Option<&str>], + [get_team_members_deserialized_common, Vec, team_id: &str, pat: Option<&str>], + [get_project_members, ServiceResponse, id_or_slug: &str, pat: Option<&str>], + [get_project_members_deserialized_common, Vec, id_or_slug: &str, pat: Option<&str>], + [get_organization_members, ServiceResponse, id_or_title: &str, pat: Option<&str>], + [get_organization_members_deserialized_common, Vec, id_or_title: &str, pat: Option<&str>], + [join_team, ServiceResponse, team_id: &str, pat: Option<&str>], + [remove_from_team, ServiceResponse, team_id: &str, user_id: &str, pat: Option<&str>], + [edit_team_member, ServiceResponse, team_id: &str, user_id: &str, patch: serde_json::Value, pat: Option<&str>], + [transfer_team_ownership, ServiceResponse, team_id: &str, user_id: &str, pat: Option<&str>], + [get_user_notifications, ServiceResponse, user_id: &str, pat: Option<&str>], + [get_user_notifications_deserialized_common, Vec, user_id: &str, pat: Option<&str>], + [mark_notification_read, ServiceResponse, notification_id: &str, pat: Option<&str>], + [add_user_to_team, ServiceResponse, team_id: &str, user_id: &str, project_permissions: Option, organization_permissions: Option, pat: Option<&str>], + [delete_notification, ServiceResponse, notification_id: &str, pat: Option<&str>], } ); delegate_api_variant!( #[async_trait(?Send)] impl ApiVersion for GenericApi { - [add_public_version, ServiceResponse, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: &str], - [add_public_version_deserialized_common, CommonVersion, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: &str], - [get_version, ServiceResponse, id_or_slug: &str, pat: &str], - [get_version_deserialized_common, CommonVersion, id_or_slug: &str, pat: &str], - [get_versions, ServiceResponse, ids_or_slugs: Vec, pat: &str], - [get_versions_deserialized_common, Vec, ids_or_slugs: Vec, pat: &str], - [edit_version, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: &str], - [get_version_from_hash, ServiceResponse, id_or_slug: &str, hash: &str, pat: &str], - [get_version_from_hash_deserialized_common, CommonVersion, id_or_slug: &str, hash: &str, pat: &str], - [get_versions_from_hashes, ServiceResponse, hashes: &[&str], algorithm: &str, pat: &str], - [get_versions_from_hashes_deserialized_common, HashMap, hashes: &[&str], algorithm: &str, pat: &str], - [get_update_from_hash, ServiceResponse, hash: &str, algorithm: &str, loaders: Option>,game_versions: Option>, version_types: Option>, pat: &str], - [get_update_from_hash_deserialized_common, CommonVersion, hash: &str, algorithm: &str,loaders: Option>,game_versions: Option>,version_types: Option>, pat: &str], - [update_files, ServiceResponse, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: &str], - [update_files_deserialized_common, HashMap, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: &str], - [get_project_versions, ServiceResponse, project_id_slug: &str, game_versions: Option>,loaders: Option>,featured: Option, version_type: Option, limit: Option, offset: Option,pat: &str], - [get_project_versions_deserialized_common, Vec, project_id_slug: &str, game_versions: Option>, loaders: Option>,featured: Option,version_type: Option,limit: Option,offset: Option,pat: &str], - [edit_version_ordering, ServiceResponse, version_id: &str,ordering: Option,pat: &str], + [add_public_version, ServiceResponse, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: Option<&str>], + [add_public_version_deserialized_common, CommonVersion, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: Option<&str>], + [get_version, ServiceResponse, id_or_slug: &str, pat: Option<&str>], + [get_version_deserialized_common, CommonVersion, id_or_slug: &str, pat: Option<&str>], + [get_versions, ServiceResponse, ids_or_slugs: Vec, pat: Option<&str>], + [get_versions_deserialized_common, Vec, ids_or_slugs: Vec, pat: Option<&str>], + [edit_version, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: Option<&str>], + [get_version_from_hash, ServiceResponse, id_or_slug: &str, hash: &str, pat: Option<&str>], + [get_version_from_hash_deserialized_common, CommonVersion, id_or_slug: &str, hash: &str, pat: Option<&str>], + [get_versions_from_hashes, ServiceResponse, hashes: &[&str], algorithm: &str, pat: Option<&str>], + [get_versions_from_hashes_deserialized_common, HashMap, hashes: &[&str], algorithm: &str, pat: Option<&str>], + [get_update_from_hash, ServiceResponse, hash: &str, algorithm: &str, loaders: Option>,game_versions: Option>, version_types: Option>, pat: Option<&str>], + [get_update_from_hash_deserialized_common, CommonVersion, hash: &str, algorithm: &str,loaders: Option>,game_versions: Option>,version_types: Option>, pat: Option<&str>], + [update_files, ServiceResponse, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: Option<&str>], + [update_files_deserialized_common, HashMap, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: Option<&str>], + [get_project_versions, ServiceResponse, project_id_slug: &str, game_versions: Option>,loaders: Option>,featured: Option, version_type: Option, limit: Option, offset: Option,pat: Option<&str>], + [get_project_versions_deserialized_common, Vec, project_id_slug: &str, game_versions: Option>, loaders: Option>,featured: Option,version_type: Option,limit: Option,offset: Option,pat: Option<&str>], + [edit_version_ordering, ServiceResponse, version_id: &str,ordering: Option,pat: Option<&str>], + [upload_file_to_version, ServiceResponse, version_id: &str, file: &TestFile, pat: Option<&str>], + [remove_version, ServiceResponse, version_id: &str, pat: Option<&str>], + [remove_version_file, ServiceResponse, hash: &str, pat: Option<&str>], } ); diff --git a/tests/common/api_common/mod.rs b/tests/common/api_common/mod.rs index 9ce71c01..0fa7faf9 100644 --- a/tests/common/api_common/mod.rs +++ b/tests/common/api_common/mod.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; use self::models::{ - CommonCategoryData, CommonImageData, CommonLoaderData, CommonNotification, CommonProject, + CommonCategoryData, CommonItemType, CommonLoaderData, CommonNotification, CommonProject, CommonTeamMember, CommonVersion, }; -use self::request_data::ProjectCreationRequestData; +use self::request_data::{ImageData, ProjectCreationRequestData}; use actix_web::dev::ServiceResponse; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use labrinth::{ models::{ projects::{ProjectId, VersionType}, @@ -38,12 +39,12 @@ pub trait ApiProject { slug: &str, version_jar: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> (CommonProject, Vec); async fn create_project( &self, creation_data: ProjectCreationRequestData, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn get_public_project_creation_data_json( &self, @@ -51,33 +52,81 @@ pub trait ApiProject { version_jar: Option<&TestFile>, ) -> serde_json::Value; - async fn remove_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; - async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; - async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject; - async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse; + async fn remove_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_project_deserialized_common( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> CommonProject; + async fn get_user_projects( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_user_projects_deserialized_common( &self, user_id_or_username: &str, - pat: &str, + pat: Option<&str>, ) -> Vec; async fn edit_project( &self, id_or_slug: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn edit_project_bulk( &self, ids_or_slugs: &[&str], patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn edit_project_icon( &self, id_or_slug: &str, - icon: Option, - pat: &str, + icon: Option, + pat: Option<&str>, + ) -> ServiceResponse; + async fn schedule_project( + &self, + id_or_slug: &str, + requested_status: &str, + date: DateTime, + pat: Option<&str>, + ) -> ServiceResponse; + #[allow(clippy::too_many_arguments)] + async fn add_gallery_item( + &self, + id_or_slug: &str, + image: ImageData, + featured: bool, + title: Option, + description: Option, + ordering: Option, + pat: Option<&str>, + ) -> ServiceResponse; + async fn remove_gallery_item( + &self, + id_or_slug: &str, + url: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn edit_gallery_item( + &self, + id_or_slug: &str, + url: &str, + patch: HashMap, + pat: Option<&str>, + ) -> ServiceResponse; + async fn create_report( + &self, + report_type: &str, + id: &str, + item_type: CommonItemType, + body: &str, + pat: Option<&str>, ) -> ServiceResponse; + async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse; } #[async_trait(?Send)] @@ -90,55 +139,72 @@ pub trait ApiTags { #[async_trait(?Send)] pub trait ApiTeams { - async fn get_team_members(&self, team_id: &str, pat: &str) -> ServiceResponse; + async fn get_team_members(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse; async fn get_team_members_deserialized_common( &self, team_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec; - async fn get_project_members(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; + async fn get_project_members(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; async fn get_project_members_deserialized_common( &self, id_or_slug: &str, - pat: &str, + pat: Option<&str>, ) -> Vec; - async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse; + async fn get_organization_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn get_organization_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec; - async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse; - async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse; + async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse; + async fn remove_from_team( + &self, + team_id: &str, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn edit_team_member( &self, team_id: &str, user_id: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn transfer_team_ownership( &self, team_id: &str, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; - async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse; + async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse; async fn get_user_notifications_deserialized_common( &self, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec; - async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse; + async fn mark_notification_read( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse; async fn add_user_to_team( &self, team_id: &str, user_id: &str, project_permissions: Option, organization_permissions: Option, - pat: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn delete_notification( + &self, + notification_id: &str, + pat: Option<&str>, ) -> ServiceResponse; - async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse; } #[async_trait(?Send)] @@ -150,7 +216,7 @@ pub trait ApiVersion { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn add_public_version_deserialized_common( &self, @@ -159,45 +225,49 @@ pub trait ApiVersion { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> CommonVersion; - async fn get_version(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; - async fn get_version_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonVersion; - async fn get_versions(&self, ids_or_slugs: Vec, pat: &str) -> ServiceResponse; + async fn get_version(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn get_version_deserialized_common( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> CommonVersion; + async fn get_versions(&self, ids_or_slugs: Vec, pat: Option<&str>) -> ServiceResponse; async fn get_versions_deserialized_common( &self, ids_or_slugs: Vec, - pat: &str, + pat: Option<&str>, ) -> Vec; async fn edit_version( &self, id_or_slug: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn get_version_from_hash( &self, id_or_slug: &str, hash: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn get_version_from_hash_deserialized_common( &self, id_or_slug: &str, hash: &str, - pat: &str, + pat: Option<&str>, ) -> CommonVersion; async fn get_versions_from_hashes( &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn get_versions_from_hashes_deserialized_common( &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> HashMap; async fn get_update_from_hash( &self, @@ -206,7 +276,7 @@ pub trait ApiVersion { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn get_update_from_hash_deserialized_common( &self, @@ -215,7 +285,7 @@ pub trait ApiVersion { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> CommonVersion; async fn update_files( &self, @@ -224,7 +294,7 @@ pub trait ApiVersion { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; async fn update_files_deserialized_common( &self, @@ -233,7 +303,7 @@ pub trait ApiVersion { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> HashMap; #[allow(clippy::too_many_arguments)] async fn get_project_versions( @@ -245,7 +315,7 @@ pub trait ApiVersion { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse; #[allow(clippy::too_many_arguments)] async fn get_project_versions_deserialized_common( @@ -257,12 +327,34 @@ pub trait ApiVersion { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> Vec; async fn edit_version_ordering( &self, version_id: &str, ordering: Option, - pat: &str, + pat: Option<&str>, + ) -> ServiceResponse; + async fn upload_file_to_version( + &self, + version_id: &str, + file: &TestFile, + pat: Option<&str>, ) -> ServiceResponse; + async fn remove_version(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse; + async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse; +} + +pub trait AppendsOptionalPat { + fn append_pat(self, pat: Option<&str>) -> Self; +} +// Impl this on all actix_web::test::TestRequest +impl AppendsOptionalPat for actix_web::test::TestRequest { + fn append_pat(self, pat: Option<&str>) -> Self { + if let Some(pat) = pat { + self.append_header(("Authorization", pat)) + } else { + self + } + } } diff --git a/tests/common/api_common/models.rs b/tests/common/api_common/models.rs index 7a48b88a..d1c13b81 100644 --- a/tests/common/api_common/models.rs +++ b/tests/common/api_common/models.rs @@ -11,7 +11,7 @@ use labrinth::models::{ users::{User, UserId}, }; use rust_decimal::Decimal; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; // Fields shared by every version of the API. // No struct in here should have ANY field that @@ -73,13 +73,6 @@ pub struct CommonVersion { pub ordering: Option, } -#[derive(Deserialize)] -pub struct CommonImageData { - pub filename: String, - pub extension: String, - pub icon: Vec, -} - #[derive(Deserialize)] pub struct CommonLoaderData { pub icon: String, @@ -125,3 +118,23 @@ pub struct CommonNotification { pub struct CommonNotificationAction { pub action_route: (String, String), } + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum CommonItemType { + Project, + Version, + User, + Unknown, +} + +impl CommonItemType { + pub fn as_str(&self) -> &'static str { + match self { + CommonItemType::Project => "project", + CommonItemType::Version => "version", + CommonItemType::User => "user", + CommonItemType::Unknown => "unknown", + } + } +} diff --git a/tests/common/api_v2/project.rs b/tests/common/api_v2/project.rs index 990e5f29..56fe12b3 100644 --- a/tests/common/api_v2/project.rs +++ b/tests/common/api_v2/project.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use crate::common::{ api_common::{ - models::{CommonImageData, CommonProject, CommonVersion}, - request_data::ProjectCreationRequestData, - Api, ApiProject, + models::{CommonItemType, CommonProject, CommonVersion}, + request_data::{ImageData, ProjectCreationRequestData}, + Api, ApiProject, AppendsOptionalPat, }, dummy_data::TestFile, }; @@ -13,6 +15,7 @@ use actix_web::{ }; use async_trait::async_trait; use bytes::Bytes; +use chrono::{DateTime, Utc}; use labrinth::{ models::v2::{projects::LegacyProject, search::LegacySearchResults}, util::actix::AppendsMultipart, @@ -27,7 +30,11 @@ use super::{ }; impl ApiV2 { - pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> LegacyProject { + pub async fn get_project_deserialized( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> LegacyProject { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -36,7 +43,7 @@ impl ApiV2 { pub async fn get_user_projects_deserialized( &self, user_id_or_username: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_projects(user_id_or_username, pat).await; assert_eq!(resp.status(), 200); @@ -47,7 +54,7 @@ impl ApiV2 { &self, query: Option<&str>, facets: Option, - pat: &str, + pat: Option<&str>, ) -> LegacySearchResults { let query_field = if let Some(query) = query { format!("&query={}", urlencoding::encode(query)) @@ -63,7 +70,7 @@ impl ApiV2 { let req = test::TestRequest::get() .uri(&format!("/v2/search?{}{}", query_field, facets_field)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; let status = resp.status(); @@ -79,7 +86,7 @@ impl ApiProject for ApiV2 { slug: &str, version_jar: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> (CommonProject, Vec) { let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); @@ -91,7 +98,7 @@ impl ApiProject for ApiV2 { // Approve as a moderator. let req = TestRequest::patch() .uri(&format!("/v2/project/{}", slug)) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .set_json(json!( { "status": "approved" @@ -106,7 +113,7 @@ impl ApiProject for ApiV2 { // Get project's versions let req = TestRequest::get() .uri(&format!("/v2/project/{}/version", slug)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; let versions: Vec = test::read_body_json(resp).await; @@ -125,35 +132,38 @@ impl ApiProject for ApiV2 { async fn create_project( &self, creation_data: ProjectCreationRequestData, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = TestRequest::post() .uri("/v2/project") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_multipart(creation_data.segment_data) .to_request(); self.call(req).await } - async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { + async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/project/{project_slug_or_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); - let resp = self.call(req).await; - assert_eq!(resp.status(), 204); - resp + + self.call(req).await } - async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { + async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/project/{id_or_slug}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject { + async fn get_project_deserialized_common( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> CommonProject { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -163,10 +173,14 @@ impl ApiProject for ApiV2 { serde_json::from_value(value).unwrap() } - async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { + async fn get_user_projects( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{}/projects", user_id_or_username)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -174,7 +188,7 @@ impl ApiProject for ApiV2 { async fn get_user_projects_deserialized_common( &self, user_id_or_username: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_projects(user_id_or_username, pat).await; assert_eq!(resp.status(), 200); @@ -189,11 +203,11 @@ impl ApiProject for ApiV2 { &self, id_or_slug: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/project/{id_or_slug}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -204,7 +218,7 @@ impl ApiProject for ApiV2 { &self, ids_or_slugs: &[&str], patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let projects_str = ids_or_slugs .iter() @@ -216,7 +230,7 @@ impl ApiProject for ApiV2 { "/v2/projects?ids={encoded}", encoded = urlencoding::encode(&format!("[{projects_str}]")) )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -226,8 +240,8 @@ impl ApiProject for ApiV2 { async fn edit_project_icon( &self, id_or_slug: &str, - icon: Option, - pat: &str, + icon: Option, + pat: Option<&str>, ) -> ServiceResponse { if let Some(icon) = icon { // If an icon is provided, upload it @@ -236,7 +250,7 @@ impl ApiProject for ApiV2 { "/v2/project/{id_or_slug}/icon?ext={ext}", ext = icon.extension )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_payload(Bytes::from(icon.icon)) .to_request(); @@ -245,10 +259,143 @@ impl ApiProject for ApiV2 { // If no icon is provided, delete the icon let req = test::TestRequest::delete() .uri(&format!("/v2/project/{id_or_slug}/icon")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } } + + async fn create_report( + &self, + report_type: &str, + id: &str, + item_type: CommonItemType, + body: &str, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v3/report") + .append_pat(pat) + .set_json(json!( + { + "report_type": report_type, + "item_id": id, + "item_type": item_type.as_str(), + "body": body, + } + )) + .to_request(); + + self.call(req).await + } + + async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/report/{id}", id = id)) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + + async fn schedule_project( + &self, + id_or_slug: &str, + requested_status: &str, + date: DateTime, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v2/version/{id_or_slug}/schedule")) + .set_json(json!( + { + "requested_status": requested_status, + "time": date, + } + )) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + + #[allow(clippy::too_many_arguments)] + async fn add_gallery_item( + &self, + id_or_slug: &str, + image: ImageData, + featured: bool, + title: Option, + description: Option, + ordering: Option, + pat: Option<&str>, + ) -> ServiceResponse { + let mut url = format!( + "/v2/project/{id_or_slug}/gallery?ext={ext}&featured={featured}", + ext = image.extension, + featured = featured + ); + if let Some(title) = title { + url.push_str(&format!("&title={}", title)); + } + if let Some(description) = description { + url.push_str(&format!("&description={}", description)); + } + if let Some(ordering) = ordering { + url.push_str(&format!("&ordering={}", ordering)); + } + + let req = test::TestRequest::post() + .uri(&url) + .append_pat(pat) + .set_payload(Bytes::from(image.icon)) + .to_request(); + + self.call(req).await + } + + async fn edit_gallery_item( + &self, + id_or_slug: &str, + image_url: &str, + patch: HashMap, + pat: Option<&str>, + ) -> ServiceResponse { + let mut url = format!( + "/v2/project/{id_or_slug}/gallery?url={image_url}", + image_url = urlencoding::encode(image_url) + ); + + for (key, value) in patch { + url.push_str(&format!( + "&{key}={value}", + key = key, + value = urlencoding::encode(&value) + )); + } + + let req = test::TestRequest::patch() + .uri(&url) + .append_pat(pat) + .to_request(); + self.call(req).await + } + + async fn remove_gallery_item( + &self, + id_or_slug: &str, + url: &str, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!( + "/v2/project/{id_or_slug}/gallery?url={url}", + url = url + )) + .append_pat(pat) + .to_request(); + + self.call(req).await + } } diff --git a/tests/common/api_v2/request_data.rs b/tests/common/api_v2/request_data.rs index 90bac7e2..1d2065ff 100644 --- a/tests/common/api_v2/request_data.rs +++ b/tests/common/api_v2/request_data.rs @@ -2,8 +2,8 @@ use serde_json::json; use crate::common::{ - api_common::request_data::{ImageData, ProjectCreationRequestData, VersionCreationRequestData}, - dummy_data::{DummyImage, TestFile}, + api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData}, + dummy_data::TestFile, }; use labrinth::{ models::projects::ProjectId, @@ -123,11 +123,3 @@ pub fn get_public_creation_data_multipart( 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_v2/tags.rs b/tests/common/api_v2/tags.rs index c67e9594..08fb4b88 100644 --- a/tests/common/api_v2/tags.rs +++ b/tests/common/api_v2/tags.rs @@ -3,12 +3,14 @@ use actix_web::{ test::{self, TestRequest}, }; use async_trait::async_trait; -use labrinth::routes::v2::tags::{CategoryData, GameVersionQueryData, LoaderData}; +use labrinth::routes::v2::tags::{ + CategoryData, DonationPlatformQueryData, GameVersionQueryData, LoaderData, +}; use crate::common::{ api_common::{ models::{CommonCategoryData, CommonLoaderData}, - Api, ApiTags, + Api, ApiTags, AppendsOptionalPat, }, database::ADMIN_USER_PAT, }; @@ -21,7 +23,7 @@ impl ApiV2 { async fn get_side_types(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v2/tag/side_type") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -35,7 +37,7 @@ impl ApiV2 { pub async fn get_game_versions(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v2/tag/game_version") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -57,6 +59,21 @@ impl ApiV2 { assert_eq!(resp.status(), 200); test::read_body_json(resp).await } + + pub async fn get_donation_platforms(&self) -> ServiceResponse { + let req = TestRequest::get() + .uri("/v2/tag/donation_platform") + .append_pat(ADMIN_USER_PAT) + .to_request(); + self.call(req).await + } + + pub async fn get_donation_platforms_deserialized(&self) -> Vec { + let resp = self.get_donation_platforms().await; + println!("Response: {:?}", resp.response().body()); + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } } #[async_trait(?Send)] @@ -64,7 +81,7 @@ impl ApiTags for ApiV2 { async fn get_loaders(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v2/tag/loader") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -82,7 +99,7 @@ impl ApiTags for ApiV2 { async fn get_categories(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v2/tag/category") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } diff --git a/tests/common/api_v2/team.rs b/tests/common/api_v2/team.rs index de35f206..49a959fe 100644 --- a/tests/common/api_v2/team.rs +++ b/tests/common/api_v2/team.rs @@ -10,7 +10,7 @@ use serde_json::json; use crate::common::{ api_common::{ models::{CommonNotification, CommonTeamMember}, - Api, ApiTeams, + Api, ApiTeams, AppendsOptionalPat, }, asserts::assert_status, }; @@ -21,7 +21,7 @@ impl ApiV2 { pub async fn get_organization_members_deserialized( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_organization_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -31,7 +31,7 @@ impl ApiV2 { pub async fn get_team_members_deserialized( &self, team_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_team_members(team_id, pat).await; assert_eq!(resp.status(), 200); @@ -41,7 +41,7 @@ impl ApiV2 { pub async fn get_user_notifications_deserialized( &self, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_eq!(resp.status(), 200); @@ -51,10 +51,10 @@ impl ApiV2 { #[async_trait(?Send)] impl ApiTeams for ApiV2 { - async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/team/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -62,7 +62,7 @@ impl ApiTeams for ApiV2 { async fn get_team_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_team_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -72,10 +72,10 @@ impl ApiTeams for ApiV2 { test::read_body_json(resp).await } - async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/project/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -83,7 +83,7 @@ impl ApiTeams for ApiV2 { async fn get_project_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_project_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -93,10 +93,14 @@ impl ApiTeams for ApiV2 { test::read_body_json(resp).await } - async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_organization_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/organization/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -104,7 +108,7 @@ impl ApiTeams for ApiV2 { async fn get_organization_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_organization_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -114,18 +118,23 @@ impl ApiTeams for ApiV2 { test::read_body_json(resp).await } - async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v2/team/{team_id}/join")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse { + async fn remove_from_team( + &self, + team_id: &str, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/team/{team_id}/members/{user_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -135,11 +144,11 @@ impl ApiTeams for ApiV2 { team_id: &str, user_id: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/team/{team_id}/members/{user_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); self.call(req).await @@ -149,11 +158,11 @@ impl ApiTeams for ApiV2 { &self, team_id: &str, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/team/{team_id}/owner")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "user_id": user_id, })) @@ -161,10 +170,10 @@ impl ApiTeams for ApiV2 { self.call(req).await } - async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { + async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{user_id}/notifications")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -172,7 +181,7 @@ impl ApiTeams for ApiV2 { async fn get_user_notifications_deserialized_common( &self, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); @@ -183,10 +192,14 @@ impl ApiTeams for ApiV2 { serde_json::from_value(value).unwrap() } - async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn mark_notification_read( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/notification/{notification_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -197,11 +210,11 @@ impl ApiTeams for ApiV2 { user_id: &str, project_permissions: Option, organization_permissions: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v2/team/{team_id}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!( { "user_id": user_id, "permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(), @@ -211,10 +224,14 @@ impl ApiTeams for ApiV2 { self.call(req).await } - async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn delete_notification( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/notification/{notification_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } diff --git a/tests/common/api_v2/version.rs b/tests/common/api_v2/version.rs index cbfcde0b..6234f814 100644 --- a/tests/common/api_v2/version.rs +++ b/tests/common/api_v2/version.rs @@ -1,12 +1,15 @@ use std::collections::HashMap; -use super::{request_data::get_public_version_creation_data, ApiV2}; +use super::{ + request_data::{self, get_public_version_creation_data}, + ApiV2, +}; use crate::common::{ - api_common::{models::CommonVersion, Api, ApiVersion}, + api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat}, asserts::assert_status, dummy_data::TestFile, }; -use actix_http::{header::AUTHORIZATION, StatusCode}; +use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, @@ -28,7 +31,7 @@ pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { } impl ApiV2 { - pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> LegacyVersion { + pub async fn get_version_deserialized(&self, id: &str, pat: Option<&str>) -> LegacyVersion { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -38,7 +41,7 @@ impl ApiV2 { &self, hash: &str, algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> LegacyVersion { let resp = self.get_version_from_hash(hash, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -49,7 +52,7 @@ impl ApiV2 { &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -60,11 +63,11 @@ impl ApiV2 { &self, algorithm: &str, hashes: Vec, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri("/v2/version_files/update_individual") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "algorithm": algorithm, "hashes": hashes @@ -77,7 +80,7 @@ impl ApiV2 { &self, algorithm: &str, hashes: Vec, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self.update_individual_files(algorithm, hashes, pat).await; assert_eq!(resp.status(), 200); @@ -94,7 +97,7 @@ impl ApiVersion for ApiV2 { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let creation_data = get_public_version_creation_data( project_id, @@ -107,7 +110,7 @@ impl ApiVersion for ApiV2 { // Add a project. let req = TestRequest::post() .uri("/v2/version") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_multipart(creation_data.segment_data) .to_request(); self.call(req).await @@ -120,7 +123,7 @@ impl ApiVersion for ApiV2 { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self .add_public_version( @@ -140,15 +143,15 @@ impl ApiVersion for ApiV2 { serde_json::from_value(value).unwrap() } - async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { + async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/version/{id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn get_version_deserialized_common(&self, id: &str, pat: &str) -> CommonVersion { + async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -162,11 +165,11 @@ impl ApiVersion for ApiV2 { &self, version_id: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/version/{version_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -177,11 +180,11 @@ impl ApiVersion for ApiV2 { &self, hash: &str, algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -190,7 +193,7 @@ impl ApiVersion for ApiV2 { &self, hash: &str, algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self.get_version_from_hash(hash, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -205,11 +208,11 @@ impl ApiVersion for ApiV2 { &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = TestRequest::post() .uri("/v2/version_files") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "hashes": hashes, "algorithm": algorithm, @@ -222,7 +225,7 @@ impl ApiVersion for ApiV2 { &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -240,13 +243,13 @@ impl ApiVersion for ApiV2 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!( "/v2/version_file/{hash}/update?algorithm={algorithm}" )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "loaders": loaders, "game_versions": game_versions, @@ -263,7 +266,7 @@ impl ApiVersion for ApiV2 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) @@ -283,11 +286,11 @@ impl ApiVersion for ApiV2 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri("/v2/version_files/update") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "algorithm": algorithm, "hashes": hashes, @@ -306,7 +309,7 @@ impl ApiVersion for ApiV2 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self .update_files( @@ -337,7 +340,7 @@ impl ApiVersion for ApiV2 { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let mut query_string = String::new(); if let Some(game_versions) = game_versions { @@ -372,7 +375,7 @@ impl ApiVersion for ApiV2 { "/v2/project/{project_id_slug}/version?{}", query_string.trim_start_matches('&') )) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -387,7 +390,7 @@ impl ApiVersion for ApiV2 { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self .get_project_versions( @@ -413,7 +416,7 @@ impl ApiVersion for ApiV2 { &self, version_id: &str, ordering: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let request = test::TestRequest::patch() .uri(&format!("/v2/version/{version_id}")) @@ -422,16 +425,16 @@ impl ApiVersion for ApiV2 { "ordering": ordering } )) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(request).await } - async fn get_versions(&self, version_ids: Vec, pat: &str) -> ServiceResponse { + async fn get_versions(&self, version_ids: Vec, pat: Option<&str>) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v2/versions?ids={}", ids)) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(request).await } @@ -439,7 +442,7 @@ impl ApiVersion for ApiV2 { async fn get_versions_deserialized_common( &self, version_ids: Vec, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_versions(version_ids, pat).await; assert_status(&resp, StatusCode::OK); @@ -449,4 +452,40 @@ impl ApiVersion for ApiV2 { let value = serde_json::to_value(v).unwrap(); serde_json::from_value(value).unwrap() } + + async fn upload_file_to_version( + &self, + version_id: &str, + file: &TestFile, + pat: Option<&str>, + ) -> ServiceResponse { + let m = request_data::get_public_creation_data_multipart( + &json!({ + "file_parts": [file.filename()] + }), + Some(file), + ); + let request = test::TestRequest::post() + .uri(&format!("/v2/version/{version_id}/file")) + .append_pat(pat) + .set_multipart(m) + .to_request(); + self.call(request).await + } + + async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse { + let request = test::TestRequest::delete() + .uri(&format!("/v2/version/{version_id}")) + .append_pat(pat) + .to_request(); + self.call(request).await + } + + async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse { + let request = test::TestRequest::delete() + .uri(&format!("/v2/version_file/{hash}")) + .append_pat(pat) + .to_request(); + self.call(request).await + } } diff --git a/tests/common/api_v3/oauth.rs b/tests/common/api_v3/oauth.rs index a1a93add..ac75a9c7 100644 --- a/tests/common/api_v3/oauth.rs +++ b/tests/common/api_v3/oauth.rs @@ -10,7 +10,10 @@ use labrinth::auth::oauth::{ }; use reqwest::header::{AUTHORIZATION, LOCATION}; -use crate::common::{api_common::Api, asserts::assert_status}; +use crate::common::{ + api_common::{Api, AppendsOptionalPat}, + asserts::assert_status, +}; use super::ApiV3; @@ -22,7 +25,7 @@ impl ApiV3 { scope: Option<&str>, redirect_uri: Option<&str>, state: Option<&str>, - user_pat: &str, + user_pat: Option<&str>, ) -> String { let auth_resp = self .oauth_authorize(client_id, scope, redirect_uri, state, user_pat) @@ -42,21 +45,18 @@ impl ApiV3 { scope: Option<&str>, redirect_uri: Option<&str>, state: Option<&str>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let uri = generate_authorize_uri(client_id, scope, redirect_uri, state); - let req = TestRequest::get() - .uri(&uri) - .append_header((AUTHORIZATION, pat)) - .to_request(); + let req = TestRequest::get().uri(&uri).append_pat(pat).to_request(); self.call(req).await } - pub async fn oauth_accept(&self, flow: &str, pat: &str) -> ServiceResponse { + pub async fn oauth_accept(&self, flow: &str, pat: Option<&str>) -> ServiceResponse { self.call( TestRequest::post() .uri("/_internal/oauth/accept") - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), }) @@ -65,11 +65,11 @@ impl ApiV3 { .await } - pub async fn oauth_reject(&self, flow: &str, pat: &str) -> ServiceResponse { + pub async fn oauth_reject(&self, flow: &str, pat: Option<&str>) -> ServiceResponse { self.call( TestRequest::post() .uri("/_internal/oauth/reject") - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), }) diff --git a/tests/common/api_v3/oauth_clients.rs b/tests/common/api_v3/oauth_clients.rs index dfad4fc2..8fe50ae9 100644 --- a/tests/common/api_v3/oauth_clients.rs +++ b/tests/common/api_v3/oauth_clients.rs @@ -10,10 +10,12 @@ use labrinth::{ }, routes::v3::oauth_clients::OAuthClientEdit, }; -use reqwest::header::AUTHORIZATION; use serde_json::json; -use crate::common::{api_common::Api, asserts::assert_status}; +use crate::common::{ + api_common::{Api, AppendsOptionalPat}, + asserts::assert_status, +}; use super::ApiV3; @@ -23,12 +25,12 @@ impl ApiV3 { name: String, max_scopes: Scopes, redirect_uris: Vec, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let max_scopes = max_scopes.bits(); let req = TestRequest::post() .uri("/_internal/oauth/app") - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .set_json(json!({ "name": name, "max_scopes": max_scopes, @@ -39,10 +41,14 @@ impl ApiV3 { self.call(req).await } - pub async fn get_user_oauth_clients(&self, user_id: &str, pat: &str) -> Vec { + pub async fn get_user_oauth_clients( + &self, + user_id: &str, + pat: Option<&str>, + ) -> Vec { let req = TestRequest::get() .uri(&format!("/v3/user/{}/oauth_apps", user_id)) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; assert_status(&resp, StatusCode::OK); @@ -50,10 +56,10 @@ impl ApiV3 { test::read_body_json(resp).await } - pub async fn get_oauth_client(&self, client_id: String, pat: &str) -> ServiceResponse { + pub async fn get_oauth_client(&self, client_id: String, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/_internal/oauth/app/{}", client_id)) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(req).await @@ -63,7 +69,7 @@ impl ApiV3 { &self, client_id: &str, edit: OAuthClientEdit, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = TestRequest::patch() .uri(&format!( @@ -71,36 +77,43 @@ impl ApiV3 { urlencoding::encode(client_id) )) .set_json(edit) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(req).await } - pub async fn delete_oauth_client(&self, client_id: &str, pat: &str) -> ServiceResponse { + pub async fn delete_oauth_client(&self, client_id: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::delete() .uri(&format!("/_internal/oauth/app/{}", client_id)) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(req).await } - pub async fn revoke_oauth_authorization(&self, client_id: &str, pat: &str) -> ServiceResponse { + pub async fn revoke_oauth_authorization( + &self, + client_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = TestRequest::delete() .uri(&format!( "/_internal/oauth/authorizations?client_id={}", urlencoding::encode(client_id) )) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(req).await } - pub async fn get_user_oauth_authorizations(&self, pat: &str) -> Vec { + pub async fn get_user_oauth_authorizations( + &self, + pat: Option<&str>, + ) -> Vec { let req = TestRequest::get() .uri("/_internal/oauth/authorizations") - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; assert_status(&resp, StatusCode::OK); diff --git a/tests/common/api_v3/organization.rs b/tests/common/api_v3/organization.rs index 268c9a1c..57b90659 100644 --- a/tests/common/api_v3/organization.rs +++ b/tests/common/api_v3/organization.rs @@ -6,7 +6,7 @@ use bytes::Bytes; use labrinth::models::{organizations::Organization, v3::projects::Project}; use serde_json::json; -use crate::common::api_common::{request_data::ImageData, Api}; +use crate::common::api_common::{request_data::ImageData, Api, AppendsOptionalPat}; use super::ApiV3; @@ -15,11 +15,11 @@ impl ApiV3 { &self, organization_title: &str, description: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri("/v3/organization") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "name": organization_title, "description": description, @@ -28,10 +28,10 @@ impl ApiV3 { self.call(req).await } - pub async fn get_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + pub async fn get_organization(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/organization/{id_or_title}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -39,17 +39,21 @@ impl ApiV3 { pub async fn get_organization_deserialized( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Organization { let resp = self.get_organization(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_organization_projects(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + pub async fn get_organization_projects( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/organization/{id_or_title}/projects")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -57,7 +61,7 @@ impl ApiV3 { pub async fn get_organization_projects_deserialized( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_organization_projects(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -68,11 +72,11 @@ impl ApiV3 { &self, id_or_title: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/organization/{id_or_title}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -83,7 +87,7 @@ impl ApiV3 { &self, id_or_title: &str, icon: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { if let Some(icon) = icon { // If an icon is provided, upload it @@ -92,7 +96,7 @@ impl ApiV3 { "/v3/organization/{id_or_title}/icon?ext={ext}", ext = icon.extension )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_payload(Bytes::from(icon.icon)) .to_request(); @@ -101,17 +105,21 @@ impl ApiV3 { // If no icon is provided, delete the icon let req = test::TestRequest::delete() .uri(&format!("/v3/organization/{id_or_title}/icon")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } } - pub async fn delete_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + pub async fn delete_organization( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/organization/{id_or_title}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await @@ -121,11 +129,11 @@ impl ApiV3 { &self, id_or_title: &str, project_id_or_slug: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/organization/{id_or_title}/projects")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "project_id": project_id_or_slug, })) @@ -138,13 +146,13 @@ impl ApiV3 { &self, id_or_title: &str, project_id_or_slug: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!( "/v3/organization/{id_or_title}/projects/{project_id_or_slug}" )) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index 41734d7f..2c678f04 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -17,9 +17,9 @@ use serde_json::json; use crate::common::{ api_common::{ - models::{CommonImageData, CommonProject, CommonVersion}, - request_data::ProjectCreationRequestData, - Api, ApiProject, + models::{CommonItemType, CommonProject, CommonVersion}, + request_data::{ImageData, ProjectCreationRequestData}, + Api, ApiProject, AppendsOptionalPat, }, asserts::assert_status, database::MOD_USER_PAT, @@ -38,7 +38,7 @@ impl ApiProject for ApiV3 { slug: &str, version_jar: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> (CommonProject, Vec) { let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); @@ -50,7 +50,7 @@ impl ApiProject for ApiV3 { // Approve as a moderator. let req = TestRequest::patch() .uri(&format!("/v3/project/{}", slug)) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .set_json(json!( { "status": "approved" @@ -66,7 +66,7 @@ impl ApiProject for ApiV3 { // Get project's versions let req = TestRequest::get() .uri(&format!("/v3/project/{}/version", slug)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; let versions: Vec = test::read_body_json(resp).await; @@ -85,35 +85,38 @@ impl ApiProject for ApiV3 { async fn create_project( &self, creation_data: ProjectCreationRequestData, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = TestRequest::post() .uri("/v3/project") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_multipart(creation_data.segment_data) .to_request(); self.call(req).await } - async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { + async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/project/{project_slug_or_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); - let resp = self.call(req).await; - assert_eq!(resp.status(), 204); - resp + + self.call(req).await } - async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { + async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/project/{id_or_slug}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject { + async fn get_project_deserialized_common( + &self, + id_or_slug: &str, + pat: Option<&str>, + ) -> CommonProject { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -123,10 +126,14 @@ impl ApiProject for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { + async fn get_user_projects( + &self, + user_id_or_username: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{}/projects", user_id_or_username)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -134,7 +141,7 @@ impl ApiProject for ApiV3 { async fn get_user_projects_deserialized_common( &self, user_id_or_username: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_projects(user_id_or_username, pat).await; assert_eq!(resp.status(), 200); @@ -149,11 +156,11 @@ impl ApiProject for ApiV3 { &self, id_or_slug: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/project/{id_or_slug}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -164,7 +171,7 @@ impl ApiProject for ApiV3 { &self, ids_or_slugs: &[&str], patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let projects_str = ids_or_slugs .iter() @@ -176,7 +183,7 @@ impl ApiProject for ApiV3 { "/v3/projects?ids={encoded}", encoded = urlencoding::encode(&format!("[{projects_str}]")) )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -186,8 +193,8 @@ impl ApiProject for ApiV3 { async fn edit_project_icon( &self, id_or_slug: &str, - icon: Option, - pat: &str, + icon: Option, + pat: Option<&str>, ) -> ServiceResponse { if let Some(icon) = icon { // If an icon is provided, upload it @@ -196,7 +203,7 @@ impl ApiProject for ApiV3 { "/v3/project/{id_or_slug}/icon?ext={ext}", ext = icon.extension )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_payload(Bytes::from(icon.icon)) .to_request(); @@ -205,16 +212,153 @@ impl ApiProject for ApiV3 { // 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)) + .append_pat(pat) .to_request(); self.call(req).await } } + + async fn create_report( + &self, + report_type: &str, + id: &str, + item_type: CommonItemType, + body: &str, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v3/report") + .append_pat(pat) + .set_json(json!( + { + "report_type": report_type, + "item_id": id, + "item_type": item_type.as_str(), + "body": body, + } + )) + .to_request(); + + self.call(req).await + } + + async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v3/report/{id}", id = id)) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + + async fn schedule_project( + &self, + id_or_slug: &str, + requested_status: &str, + date: DateTime, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v3/version/{id_or_slug}/schedule")) + .set_json(json!( + { + "requested_status": requested_status, + "time": date, + } + )) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + + #[allow(clippy::too_many_arguments)] + async fn add_gallery_item( + &self, + id_or_slug: &str, + image: ImageData, + featured: bool, + title: Option, + description: Option, + ordering: Option, + pat: Option<&str>, + ) -> ServiceResponse { + let mut url = format!( + "/v3/project/{id_or_slug}/gallery?ext={ext}&featured={featured}", + ext = image.extension, + featured = featured + ); + if let Some(title) = title { + url.push_str(&format!("&title={}", title)); + } + if let Some(description) = description { + url.push_str(&format!("&description={}", description)); + } + if let Some(ordering) = ordering { + url.push_str(&format!("&ordering={}", ordering)); + } + + let req = test::TestRequest::post() + .uri(&url) + .append_pat(pat) + .set_payload(Bytes::from(image.icon)) + .to_request(); + + self.call(req).await + } + + async fn edit_gallery_item( + &self, + id_or_slug: &str, + image_url: &str, + patch: HashMap, + pat: Option<&str>, + ) -> ServiceResponse { + let mut url = format!( + "/v3/project/{id_or_slug}/gallery?url={image_url}", + image_url = urlencoding::encode(image_url) + ); + + for (key, value) in patch { + url.push_str(&format!( + "&{key}={value}", + key = key, + value = urlencoding::encode(&value) + )); + } + + let req = test::TestRequest::patch() + .uri(&url) + .append_pat(pat) + .to_request(); + + let t = self.call(req).await; + println!("Status: {}", t.status()); + println!("respone Body: {:?}", t.response().body()); + t + } + + async fn remove_gallery_item( + &self, + id_or_slug: &str, + url: &str, + pat: Option<&str>, + ) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!( + "/v3/project/{id_or_slug}/gallery?url={url}", + url = url + )) + .append_pat(pat) + .to_request(); + + self.call(req).await + } } impl ApiV3 { - pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project { + pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: Option<&str>) -> Project { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -224,7 +368,7 @@ impl ApiV3 { &self, query: Option<&str>, facets: Option, - pat: &str, + pat: Option<&str>, ) -> ReturnSearchResults { let query_field = if let Some(query) = query { format!("&query={}", urlencoding::encode(query)) @@ -240,7 +384,7 @@ impl ApiV3 { let req = test::TestRequest::get() .uri(&format!("/v3/search?{}{}", query_field, facets_field)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); let resp = self.call(req).await; let status = resp.status(); @@ -255,7 +399,7 @@ impl ApiV3 { start_date: Option>, end_date: Option>, resolution_minutes: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let pv_string = if ids_are_version_ids { let version_string: String = serde_json::to_string(&id_or_slugs).unwrap(); @@ -286,7 +430,7 @@ impl ApiV3 { let req = test::TestRequest::get() .uri(&format!("/v3/analytics/revenue?{pv_string}{extra_args}",)) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await @@ -299,7 +443,7 @@ impl ApiV3 { start_date: Option>, end_date: Option>, resolution_minutes: Option, - pat: &str, + pat: Option<&str>, ) -> HashMap> { let resp = self .get_analytics_revenue( diff --git a/tests/common/api_v3/request_data.rs b/tests/common/api_v3/request_data.rs index 4acc9bfe..95b3abec 100644 --- a/tests/common/api_v3/request_data.rs +++ b/tests/common/api_v3/request_data.rs @@ -2,8 +2,8 @@ use serde_json::json; use crate::common::{ - api_common::request_data::{ImageData, ProjectCreationRequestData, VersionCreationRequestData}, - dummy_data::{DummyImage, TestFile}, + api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData}, + dummy_data::TestFile, }; use labrinth::{ models::projects::ProjectId, @@ -133,11 +133,3 @@ pub fn get_public_creation_data_multipart( 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 f513e8ea..9539dab2 100644 --- a/tests/common/api_v3/tags.rs +++ b/tests/common/api_v3/tags.rs @@ -11,7 +11,7 @@ use labrinth::{ use crate::common::{ api_common::{ models::{CommonCategoryData, CommonLoaderData}, - Api, ApiTags, + Api, ApiTags, AppendsOptionalPat, }, database::ADMIN_USER_PAT, }; @@ -23,7 +23,7 @@ impl ApiTags for ApiV3 { async fn get_loaders(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/tag/loader") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -41,7 +41,7 @@ impl ApiTags for ApiV3 { async fn get_categories(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/tag/category") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -67,7 +67,7 @@ impl ApiV3 { 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)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } @@ -85,7 +85,7 @@ impl ApiV3 { async fn get_games(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/games") - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); self.call(req).await } diff --git a/tests/common/api_v3/team.rs b/tests/common/api_v3/team.rs index bb29e932..9e9f1971 100644 --- a/tests/common/api_v3/team.rs +++ b/tests/common/api_v3/team.rs @@ -10,7 +10,7 @@ use serde_json::json; use crate::common::{ api_common::{ models::{CommonNotification, CommonTeamMember}, - Api, ApiTeams, + Api, ApiTeams, AppendsOptionalPat, }, asserts::assert_status, }; @@ -21,14 +21,18 @@ impl ApiV3 { pub async fn get_organization_members_deserialized( &self, id_or_title: &str, - pat: &str, + pat: Option<&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 get_team_members_deserialized(&self, team_id: &str, pat: &str) -> Vec { + pub async fn get_team_members_deserialized( + &self, + team_id: &str, + pat: Option<&str>, + ) -> Vec { let resp = self.get_team_members(team_id, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -37,10 +41,10 @@ impl ApiV3 { #[async_trait(?Send)] impl ApiTeams for ApiV3 { - async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/team/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -48,7 +52,7 @@ impl ApiTeams for ApiV3 { async fn get_team_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_team_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -59,10 +63,10 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/project/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -70,7 +74,7 @@ impl ApiTeams for ApiV3 { async fn get_project_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_project_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -81,10 +85,14 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_organization_members( + &self, + id_or_title: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/organization/{id_or_title}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -92,7 +100,7 @@ impl ApiTeams for ApiV3 { async fn get_organization_members_deserialized_common( &self, id_or_title: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_organization_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); @@ -103,18 +111,23 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/join")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse { + async fn remove_from_team( + &self, + team_id: &str, + user_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/team/{team_id}/members/{user_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -124,11 +137,11 @@ impl ApiTeams for ApiV3 { team_id: &str, user_id: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/team/{team_id}/members/{user_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); self.call(req).await @@ -138,11 +151,11 @@ impl ApiTeams for ApiV3 { &self, team_id: &str, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/team/{team_id}/owner")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "user_id": user_id, })) @@ -150,10 +163,10 @@ impl ApiTeams for ApiV3 { self.call(req).await } - async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { + async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{user_id}/notifications")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -161,7 +174,7 @@ impl ApiTeams for ApiV3 { async fn get_user_notifications_deserialized_common( &self, user_id: &str, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); @@ -172,10 +185,14 @@ impl ApiTeams for ApiV3 { serde_json::from_value(value).unwrap() } - async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn mark_notification_read( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/notification/{notification_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -185,11 +202,11 @@ impl ApiTeams for ApiV3 { user_id: &str, project_permissions: Option, organization_permissions: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/members")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!( { "user_id": user_id, "permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(), @@ -199,10 +216,14 @@ impl ApiTeams for ApiV3 { self.call(req).await } - async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn delete_notification( + &self, + notification_id: &str, + pat: Option<&str>, + ) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/notification/{notification_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } diff --git a/tests/common/api_v3/version.rs b/tests/common/api_v3/version.rs index 8e6bba58..ac476601 100644 --- a/tests/common/api_v3/version.rs +++ b/tests/common/api_v3/version.rs @@ -1,12 +1,15 @@ use std::collections::HashMap; -use super::{request_data::get_public_version_creation_data, ApiV3}; +use super::{ + request_data::{self, get_public_version_creation_data}, + ApiV3, +}; use crate::common::{ - api_common::{models::CommonVersion, Api, ApiVersion}, + api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat}, asserts::assert_status, dummy_data::TestFile, }; -use actix_http::{header::AUTHORIZATION, StatusCode}; +use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, @@ -35,7 +38,7 @@ impl ApiV3 { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> Version { let resp = self .add_public_version( @@ -55,7 +58,7 @@ impl ApiV3 { test::read_body_json(version).await } - pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> Version { + pub async fn get_version_deserialized(&self, id: &str, pat: Option<&str>) -> Version { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await @@ -65,11 +68,11 @@ impl ApiV3 { &self, algorithm: &str, hashes: Vec, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::post() .uri("/v3/version_files/update_individual") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "algorithm": algorithm, "hashes": hashes @@ -82,7 +85,7 @@ impl ApiV3 { &self, algorithm: &str, hashes: Vec, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self.update_individual_files(algorithm, hashes, pat).await; assert_eq!(resp.status(), 200); @@ -127,7 +130,7 @@ impl ApiVersion for ApiV3 { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let creation_data = get_public_version_creation_data( project_id, @@ -140,7 +143,7 @@ impl ApiVersion for ApiV3 { // Add a versiom. let req = TestRequest::post() .uri("/v3/version") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_multipart(creation_data.segment_data) .to_request(); self.call(req).await @@ -153,7 +156,7 @@ impl ApiVersion for ApiV3 { version_jar: TestFile, ordering: Option, modify_json: Option, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self .add_public_version( @@ -173,15 +176,15 @@ impl ApiVersion for ApiV3 { serde_json::from_value(value).unwrap() } - async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { + async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/version/{id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } - async fn get_version_deserialized_common(&self, id: &str, pat: &str) -> CommonVersion { + async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); // First, deserialize to the non-common format (to test the response is valid for this api version) @@ -195,11 +198,11 @@ impl ApiVersion for ApiV3 { &self, version_id: &str, patch: serde_json::Value, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/version/{version_id}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(patch) .to_request(); @@ -210,11 +213,11 @@ impl ApiVersion for ApiV3 { &self, hash: &str, algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/version_file/{hash}?algorithm={algorithm}")) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -223,7 +226,7 @@ impl ApiVersion for ApiV3 { &self, hash: &str, algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self.get_version_from_hash(hash, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -238,11 +241,11 @@ impl ApiVersion for ApiV3 { &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let req = TestRequest::post() .uri("/v3/version_files") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json!({ "hashes": hashes, "algorithm": algorithm, @@ -255,7 +258,7 @@ impl ApiVersion for ApiV3 { &self, hashes: &[&str], algorithm: &str, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_eq!(resp.status(), 200); @@ -273,7 +276,7 @@ impl ApiVersion for ApiV3 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let mut json = json!({}); if let Some(loaders) = loaders { @@ -292,7 +295,7 @@ impl ApiVersion for ApiV3 { .uri(&format!( "/v3/version_file/{hash}/update?algorithm={algorithm}" )) - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json) .to_request(); self.call(req).await @@ -305,7 +308,7 @@ impl ApiVersion for ApiV3 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> CommonVersion { let resp = self .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) @@ -325,7 +328,7 @@ impl ApiVersion for ApiV3 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let mut json = json!({ "algorithm": algorithm, @@ -345,7 +348,7 @@ impl ApiVersion for ApiV3 { let req = test::TestRequest::post() .uri("/v3/version_files/update") - .append_header(("Authorization", pat)) + .append_pat(pat) .set_json(json) .to_request(); self.call(req).await @@ -358,7 +361,7 @@ impl ApiVersion for ApiV3 { loaders: Option>, game_versions: Option>, version_types: Option>, - pat: &str, + pat: Option<&str>, ) -> HashMap { let resp = self .update_files( @@ -389,7 +392,7 @@ impl ApiVersion for ApiV3 { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let mut query_string = String::new(); if let Some(game_versions) = game_versions { @@ -424,7 +427,7 @@ impl ApiVersion for ApiV3 { "/v3/project/{project_id_slug}/version?{}", query_string.trim_start_matches('&') )) - .append_header(("Authorization", pat)) + .append_pat(pat) .to_request(); self.call(req).await } @@ -439,7 +442,7 @@ impl ApiVersion for ApiV3 { version_type: Option, limit: Option, offset: Option, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self .get_project_versions( @@ -466,7 +469,7 @@ impl ApiVersion for ApiV3 { &self, version_id: &str, ordering: Option, - pat: &str, + pat: Option<&str>, ) -> ServiceResponse { let request = test::TestRequest::patch() .uri(&format!("/v3/version/{version_id}")) @@ -475,16 +478,16 @@ impl ApiVersion for ApiV3 { "ordering": ordering } )) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(request).await } - async fn get_versions(&self, version_ids: Vec, pat: &str) -> ServiceResponse { + async fn get_versions(&self, version_ids: Vec, pat: Option<&str>) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v3/versions?ids={}", ids)) - .append_header((AUTHORIZATION, pat)) + .append_pat(pat) .to_request(); self.call(request).await } @@ -492,7 +495,7 @@ impl ApiVersion for ApiV3 { async fn get_versions_deserialized_common( &self, version_ids: Vec, - pat: &str, + pat: Option<&str>, ) -> Vec { let resp = self.get_versions(version_ids, pat).await; assert_status(&resp, StatusCode::OK); @@ -502,4 +505,46 @@ impl ApiVersion for ApiV3 { let value = serde_json::to_value(v).unwrap(); serde_json::from_value(value).unwrap() } + + async fn upload_file_to_version( + &self, + version_id: &str, + file: &TestFile, + pat: Option<&str>, + ) -> ServiceResponse { + let m = request_data::get_public_creation_data_multipart( + &json!({ + "file_parts": [file.filename()] + }), + Some(file), + ); + let request = test::TestRequest::post() + .uri(&format!( + "/v3/version/{version_id}/file", + version_id = version_id + )) + .append_pat(pat) + .set_multipart(m) + .to_request(); + self.call(request).await + } + + async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse { + let request = test::TestRequest::delete() + .uri(&format!( + "/v3/version/{version_id}", + version_id = version_id + )) + .append_pat(pat) + .to_request(); + self.call(request).await + } + + async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse { + let request = test::TestRequest::delete() + .uri(&format!("/v3/version_file/{hash}")) + .append_pat(pat) + .to_request(); + self.call(request).await + } } diff --git a/tests/common/database.rs b/tests/common/database.rs index d3092929..7e0b8218 100644 --- a/tests/common/database.rs +++ b/tests/common/database.rs @@ -27,11 +27,11 @@ pub const FRIEND_USER_ID_PARSED: i64 = 4; pub const ENEMY_USER_ID_PARSED: i64 = 5; // These are full-scoped PATs- as if the user was logged in (including illegal scopes). -pub const ADMIN_USER_PAT: &str = "mrp_patadmin"; -pub const MOD_USER_PAT: &str = "mrp_patmoderator"; -pub const USER_USER_PAT: &str = "mrp_patuser"; -pub const FRIEND_USER_PAT: &str = "mrp_patfriend"; -pub const ENEMY_USER_PAT: &str = "mrp_patenemy"; +pub const ADMIN_USER_PAT: Option<&str> = Some("mrp_patadmin"); +pub const MOD_USER_PAT: Option<&str> = Some("mrp_patmoderator"); +pub const USER_USER_PAT: Option<&str> = Some("mrp_patuser"); +pub const FRIEND_USER_PAT: Option<&str> = Some("mrp_patfriend"); +pub const ENEMY_USER_PAT: Option<&str> = Some("mrp_patenemy"); const TEMPLATE_DATABASE_NAME: &str = "labrinth_tests_template"; diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index 44d9e238..394df369 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -16,7 +16,11 @@ use zip::{write::FileOptions, CompressionMethod, ZipWriter}; use crate::common::{api_common::Api, database::USER_USER_PAT}; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; -use super::{api_common::ApiProject, api_v3::ApiV3, database::TemporaryDatabase}; +use super::{ + api_common::{request_data::ImageData, ApiProject, AppendsOptionalPat}, + api_v3::ApiV3, + database::TemporaryDatabase, +}; use super::{asserts::assert_status, database::USER_USER_ID, get_json_val_str}; @@ -36,9 +40,11 @@ pub const DUMMY_CATEGORIES: &[&str] = &[ pub const DUMMY_OAUTH_CLIENT_ALPHA_SECRET: &str = "abcdefghijklmnopqrstuvwxyz"; #[allow(dead_code)] +#[derive(Clone)] pub enum TestFile { DummyProjectAlpha, DummyProjectBeta, + BasicZip, BasicMod, BasicModDifferent, // Randomly generates a valid .jar with a random hash. @@ -380,7 +386,7 @@ pub async fn add_project_beta(api: &ApiV3) -> (Project, Version) { // Add a project. let req = TestRequest::post() .uri("/v3/project") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); let resp = api.call(req).await; @@ -393,7 +399,7 @@ pub async fn add_organization_zeta(api: &ApiV3) -> Organization { // Add an organzation. let req = TestRequest::post() .uri("/v3/organization") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "name": "zeta", "description": "A dummy organization for testing with." @@ -410,7 +416,7 @@ pub async fn get_project_alpha(api: &ApiV3) -> (Project, Version) { // Get project let req = TestRequest::get() .uri("/v3/project/alpha") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = api.call(req).await; let project: Project = test::read_body_json(resp).await; @@ -418,7 +424,7 @@ pub async fn get_project_alpha(api: &ApiV3) -> (Project, Version) { // Get project's versions let req = TestRequest::get() .uri("/v3/project/alpha/version") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = api.call(req).await; let versions: Vec = test::read_body_json(resp).await; @@ -431,7 +437,7 @@ pub async fn get_project_beta(api: &ApiV3) -> (Project, Version) { // Get project let req = TestRequest::get() .uri("/v3/project/beta") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = api.call(req).await; assert_status(&resp, StatusCode::OK); @@ -441,7 +447,7 @@ pub async fn get_project_beta(api: &ApiV3) -> (Project, Version) { // Get project's versions let req = TestRequest::get() .uri("/v3/project/beta/version") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = api.call(req).await; assert_status(&resp, StatusCode::OK); @@ -455,7 +461,7 @@ pub async fn get_organization_zeta(api: &ApiV3) -> Organization { // Get organization let req = TestRequest::get() .uri("/v3/organization/zeta") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = api.call(req).await; let organization: Organization = test::read_body_json(resp).await; @@ -475,6 +481,7 @@ impl TestFile { match self { TestFile::DummyProjectAlpha => "dummy-project-alpha.jar", TestFile::DummyProjectBeta => "dummy-project-beta.jar", + TestFile::BasicZip => "simple-zip.zip", TestFile::BasicMod => "basic-mod.jar", TestFile::BasicModDifferent => "basic-mod-different.jar", TestFile::BasicModRandom { filename, .. } => filename, @@ -492,6 +499,7 @@ impl TestFile { include_bytes!("../../tests/files/dummy-project-beta.jar").to_vec() } TestFile::BasicMod => include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + TestFile::BasicZip => include_bytes!("../../tests/files/simple-zip.zip").to_vec(), TestFile::BasicModDifferent => { include_bytes!("../../tests/files/basic-mod-different.jar").to_vec() } @@ -508,10 +516,27 @@ impl TestFile { TestFile::BasicModDifferent => "mod", TestFile::BasicModRandom { .. } => "mod", + TestFile::BasicZip => "resourcepack", + TestFile::BasicModpackRandom { .. } => "modpack", } .to_string() } + + pub fn content_type(&self) -> Option { + match self { + TestFile::DummyProjectAlpha => Some("application/java-archive"), + TestFile::DummyProjectBeta => Some("application/java-archive"), + TestFile::BasicMod => Some("application/java-archive"), + TestFile::BasicModDifferent => Some("application/java-archive"), + TestFile::BasicModRandom { .. } => Some("application/java-archive"), + + TestFile::BasicZip => Some("application/zip"), + + TestFile::BasicModpackRandom { .. } => Some("application/x-modrinth-modpack+zip"), + } + .map(|s| s.to_string()) + } } impl DummyImage { @@ -534,4 +559,12 @@ impl DummyImage { DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(), } } + + pub fn get_icon_data(&self) -> ImageData { + ImageData { + filename: self.filename(), + extension: self.extension(), + icon: self.bytes(), + } + } } diff --git a/tests/common/environment.rs b/tests/common/environment.rs index 13802b04..a3b09e63 100644 --- a/tests/common/environment.rs +++ b/tests/common/environment.rs @@ -122,7 +122,7 @@ impl TestEnvironment { pub async fn assert_read_notifications_status( &self, user_id: &str, - pat: &str, + pat: Option<&str>, status_code: StatusCode, ) { let resp = self.api.get_user_notifications(user_id, pat).await; @@ -133,7 +133,7 @@ impl TestEnvironment { pub async fn assert_read_user_projects_status( &self, user_id: &str, - pat: &str, + pat: Option<&str>, status_code: StatusCode, ) { let resp = self.api.get_user_projects(user_id, pat).await; diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs index 4b55b8b9..39c3a737 100644 --- a/tests/common/permissions.rs +++ b/tests/common/permissions.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] use actix_http::StatusCode; -use actix_web::test::{self, TestRequest}; +use actix_web::{dev::ServiceResponse, test}; +use futures::Future; use itertools::Itertools; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; @@ -32,7 +33,7 @@ pub struct PermissionsTest<'a, A: Api> { // User ID to use for the test user, and their PAT user_id: &'a str, - user_pat: &'a str, + user_pat: Option<&'a str>, // Whether or not the user ID should be removed from the project/organization team after the test // (This is mostly reelvant if you are also using an existing project/organization, and want to do @@ -58,15 +59,14 @@ pub struct PermissionsTest<'a, A: Api> { failure_json_check: Option, success_json_check: Option, } - -pub struct PermissionsTestContext<'a> { - // pub test_env: &'a TestEnvironment, - pub user_id: &'a str, - pub user_pat: &'a str, - pub project_id: Option<&'a str>, - pub team_id: Option<&'a str>, - pub organization_id: Option<&'a str>, - pub organization_team_id: Option<&'a str>, +#[derive(Clone, Debug)] +pub struct PermissionsTestContext { + pub test_pat: Option, + pub user_id: String, + pub project_id: Option, + pub team_id: Option, + pub organization_id: Option, + pub organization_team_id: Option, } impl<'a, A: Api> PermissionsTest<'a, A> { @@ -118,7 +118,12 @@ impl<'a, A: Api> PermissionsTest<'a, A> { // 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 - pub fn with_user(mut self, user_id: &'a str, user_pat: &'a str, remove_user: bool) -> Self { + pub fn with_user( + mut self, + user_id: &'a str, + user_pat: Option<&'a str>, + remove_user: bool, + ) -> Self { self.user_id = user_id; self.user_pat = user_pat; self.remove_user = remove_user; @@ -149,21 +154,22 @@ impl<'a, A: Api> PermissionsTest<'a, A> { self } - pub async fn simple_project_permissions_test( + pub async fn simple_project_permissions_test( &self, success_permissions: ProjectPermissions, req_gen: T, ) -> Result<(), String> where - T: Fn(&PermissionsTestContext) -> TestRequest, + T: Fn(PermissionsTestContext) -> Fut, + Fut: Future, // Ensure Fut is Send and 'static { let test_env = self.test_env; let failure_project_permissions = self .failure_project_permissions .unwrap_or(ProjectPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - user_id: self.user_id, - user_pat: self.user_pat, + test_pat: None, + user_id: self.user_id.to_string(), project_id: None, team_id: None, organization_id: None, @@ -190,14 +196,12 @@ impl<'a, A: Api> PermissionsTest<'a, A> { .await; // Failure test- not logged in - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Failure permissions test failed. Expected failure codes {} got {}", @@ -215,15 +219,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { } // 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 + let resp = req_gen(PermissionsTestContext { + test_pat: ENEMY_USER_PAT.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", ENEMY_USER_PAT)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Failure permissions test failed. Expected failure codes {} got {}", @@ -241,15 +243,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { } // Failure test- logged in with EVERY non-relevant permission - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp: ServiceResponse = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Failure permissions test failed. Expected failure codes {} got {}", @@ -277,15 +277,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { .await; // Successful test - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( "Success permissions test failed. Expected success, got {}", @@ -306,21 +304,22 @@ impl<'a, A: Api> PermissionsTest<'a, A> { Ok(()) } - pub async fn simple_organization_permissions_test( + pub async fn simple_organization_permissions_test( &self, success_permissions: OrganizationPermissions, req_gen: T, ) -> Result<(), String> where - T: Fn(&PermissionsTestContext) -> TestRequest, + T: Fn(PermissionsTestContext) -> Fut, + Fut: Future, { let test_env = self.test_env; let failure_organization_permissions = self .failure_organization_permissions .unwrap_or(OrganizationPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - user_id: self.user_id, - user_pat: self.user_pat, + test_pat: None, + user_id: self.user_id.to_string(), project_id: None, team_id: None, organization_id: None, @@ -348,15 +347,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { .await; // Failure test - let request = req_gen(&PermissionsTestContext { - organization_id: Some(&organization_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + organization_id: Some(organization_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Failure permissions test failed. Expected failure codes {} got {}", @@ -379,15 +376,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { .await; // Successful test - let request = req_gen(&PermissionsTestContext { - organization_id: Some(&organization_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + organization_id: Some(organization_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( "Success permissions test failed. Expected success, got {}", @@ -403,21 +398,22 @@ impl<'a, A: Api> PermissionsTest<'a, A> { Ok(()) } - pub async fn full_project_permissions_test( + pub async fn full_project_permissions_test( &self, success_permissions: ProjectPermissions, req_gen: T, ) -> Result<(), String> where - T: Fn(&PermissionsTestContext) -> TestRequest, + T: Fn(PermissionsTestContext) -> Fut, + Fut: Future, { let test_env = self.test_env; let failure_project_permissions = self .failure_project_permissions .unwrap_or(ProjectPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - user_id: self.user_id, - user_pat: self.user_pat, + test_pat: None, + user_id: self.user_id.to_string(), project_id: None, team_id: None, organization_id: None, @@ -430,14 +426,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { let test_1 = async { let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: None, + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 1 failed. Expected failure codes {} got {}", @@ -471,14 +466,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { let test_2 = async { let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 2 failed. Expected failure codes {} got {}", @@ -521,15 +515,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 3 failed. Expected failure codes {} got {}", @@ -572,15 +564,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( "Test 4 failed. Expected success, got {}", @@ -623,15 +613,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 5 failed. Expected failure codes {} got {}", @@ -678,15 +666,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( "Test 6 failed. Expected success, got {}", @@ -739,15 +725,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 7 failed. Expected failure codes {} got {}", @@ -804,15 +788,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - project_id: Some(&project_id), - team_id: Some(&team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + project_id: Some(project_id.clone()), + team_id: Some(team_id.clone()), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( @@ -844,21 +826,22 @@ impl<'a, A: Api> PermissionsTest<'a, A> { Ok(()) } - pub async fn full_organization_permissions_tests( + pub async fn full_organization_permissions_tests( &self, success_permissions: OrganizationPermissions, req_gen: T, ) -> Result<(), String> where - T: Fn(&PermissionsTestContext) -> TestRequest, + T: Fn(PermissionsTestContext) -> Fut, + Fut: Future, { let test_env = self.test_env; let failure_organization_permissions = self .failure_organization_permissions .unwrap_or(OrganizationPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - user_id: self.user_id, - user_pat: self.user_pat, + test_pat: None, + user_id: self.user_id.to_string(), project_id: None, // Will be overwritten on each test team_id: None, // Will be overwritten on each test organization_id: None, @@ -871,14 +854,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { let (organization_id, organization_team_id) = create_dummy_org(&test_env.setup_api).await; - let request = req_gen(&PermissionsTestContext { - organization_id: Some(&organization_id), - organization_team_id: Some(&organization_team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + organization_id: Some(organization_id.clone()), + organization_team_id: Some(organization_team_id), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 1 failed. Expected failure codes {} got {}", @@ -921,15 +903,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - organization_id: Some(&organization_id), - organization_team_id: Some(&organization_team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + organization_id: Some(organization_id.clone()), + organization_team_id: Some(organization_team_id), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { return Err(format!( "Test 2 failed. Expected failure codes {} got {}", @@ -972,15 +952,13 @@ impl<'a, A: Api> PermissionsTest<'a, A> { ) .await; - let request = req_gen(&PermissionsTestContext { - organization_id: Some(&organization_id), - organization_team_id: Some(&organization_team_id), - ..test_context + let resp = req_gen(PermissionsTestContext { + test_pat: self.user_pat.map(|s| s.to_string()), + organization_id: Some(organization_id.clone()), + organization_team_id: Some(organization_team_id), + ..test_context.clone() }) - .append_header(("Authorization", self.user_pat)) - .to_request(); - - let resp = test_env.call(request).await; + .await; if !resp.status().is_success() { return Err(format!( "Test 3 failed. Expected success, got {}", @@ -1054,7 +1032,7 @@ async fn add_project_to_org(setup_api: &ApiV3, project_id: &str, organization_id async fn add_user_to_team( user_id: &str, - user_pat: &str, + user_pat: Option<&str>, team_id: &str, project_permissions: Option, organization_permissions: Option, @@ -1109,7 +1087,7 @@ async fn remove_user_from_team(user_id: &str, team_id: &str, setup_api: &ApiV3) async fn get_project_permissions( user_id: &str, - user_pat: &str, + user_pat: Option<&str>, project_id: &str, setup_api: &ApiV3, ) -> ProjectPermissions { @@ -1132,7 +1110,7 @@ async fn get_project_permissions( async fn get_organization_permissions( user_id: &str, - user_pat: &str, + user_pat: Option<&str>, organization_id: &str, setup_api: &ApiV3, ) -> OrganizationPermissions { diff --git a/tests/common/search.rs b/tests/common/search.rs index 58678f45..51058d19 100644 --- a/tests/common/search.rs +++ b/tests/common/search.rs @@ -21,7 +21,10 @@ pub async fn setup_search_projects(test_env: &TestEnvironment) -> Arc| { + |id: u64, + pat: Option<&'static str>, + is_modpack: bool, + modify_json: Option| { let slug = format!("{test_name}-searchable-project-{id}"); let jar = if is_modpack { diff --git a/tests/loader_fields.rs b/tests/loader_fields.rs index d4ed8790..f42e2102 100644 --- a/tests/loader_fields.rs +++ b/tests/loader_fields.rs @@ -370,10 +370,16 @@ async fn creating_loader_fields() { project.fields.get("game_versions").unwrap(), &[json!("1.20.1"), json!("1.20.2"), json!("1.20.5")] ); - assert_eq!( - project.fields.get("singleplayer").unwrap(), - &[json!(false), json!(true)] - ); + assert!(project + .fields + .get("singleplayer") + .unwrap() + .contains(&json!(false))); + assert!(project + .fields + .get("singleplayer") + .unwrap() + .contains(&json!(true))); }) .await } diff --git a/tests/oauth.rs b/tests/oauth.rs index da6b29a8..101cefbf 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -71,7 +71,7 @@ async fn oauth_flow_happy_path() { // Validate the token works env.assert_read_notifications_status( FRIEND_USER_ID, - &token_resp.access_token, + Some(&token_resp.access_token), StatusCode::OK, ) .await; @@ -82,7 +82,7 @@ async fn oauth_flow_happy_path() { #[actix_rt::test] async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() { with_test_environment(None, |env: TestEnvironment| async move { - let DummyOAuthClientAlpha { client_id, .. } = env.dummy.unwrap().oauth_client_alpha.clone(); + let DummyOAuthClientAlpha { client_id, .. } = env.dummy.unwrap().oauth_client_alpha; let resp = env .api @@ -119,7 +119,7 @@ async fn get_oauth_token_with_already_used_auth_code_fails() { client_id, client_secret, .. - } = env.dummy.unwrap().oauth_client_alpha.clone(); + } = env.dummy.unwrap().oauth_client_alpha; let resp = env .api @@ -179,17 +179,29 @@ async fn authorize_with_broader_scopes_can_complete_flow() { env.assert_read_notifications_status( USER_USER_ID, - &first_access_token, + Some(&first_access_token), StatusCode::UNAUTHORIZED, ) .await; - env.assert_read_user_projects_status(USER_USER_ID, &first_access_token, StatusCode::OK) - .await; + env.assert_read_user_projects_status( + USER_USER_ID, + Some(&first_access_token), + StatusCode::OK, + ) + .await; - env.assert_read_notifications_status(USER_USER_ID, &second_access_token, StatusCode::OK) - .await; - env.assert_read_user_projects_status(USER_USER_ID, &second_access_token, StatusCode::OK) - .await; + env.assert_read_notifications_status( + USER_USER_ID, + Some(&second_access_token), + StatusCode::OK, + ) + .await; + env.assert_read_user_projects_status( + USER_USER_ID, + Some(&second_access_token), + StatusCode::OK, + ) + .await; }) .await; } @@ -278,7 +290,7 @@ async fn revoke_authorization_after_issuing_token_revokes_token() { USER_USER_PAT, ) .await; - env.assert_read_notifications_status(USER_USER_ID, &access_token, StatusCode::OK) + env.assert_read_notifications_status(USER_USER_ID, Some(&access_token), StatusCode::OK) .await; let resp = env @@ -287,8 +299,12 @@ async fn revoke_authorization_after_issuing_token_revokes_token() { .await; assert_status(&resp, StatusCode::OK); - env.assert_read_notifications_status(USER_USER_ID, &access_token, StatusCode::UNAUTHORIZED) - .await; + env.assert_read_notifications_status( + USER_USER_ID, + Some(&access_token), + StatusCode::UNAUTHORIZED, + ) + .await; }) .await; } diff --git a/tests/oauth_clients.rs b/tests/oauth_clients.rs index e601b6c0..ab41c0d6 100644 --- a/tests/oauth_clients.rs +++ b/tests/oauth_clients.rs @@ -167,8 +167,12 @@ async fn delete_oauth_client_after_issuing_access_tokens_revokes_tokens() { env.api.delete_oauth_client(&client_id, USER_USER_PAT).await; - env.assert_read_notifications_status(USER_USER_ID, &access_token, StatusCode::UNAUTHORIZED) - .await; + env.assert_read_notifications_status( + USER_USER_ID, + Some(&access_token), + StatusCode::UNAUTHORIZED, + ) + .await; }) .await; } diff --git a/tests/organizations.rs b/tests/organizations.rs index 7597ef8d..02a58239 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -1,11 +1,8 @@ use crate::common::{ api_common::ApiTeams, - 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, }; -use actix_web::test; -use bytes::Bytes; use common::{ api_v3::ApiV3, database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, @@ -191,7 +188,7 @@ async fn add_remove_icon() { let resp = api .edit_organization_icon( zeta_organization_id, - Some(get_icon_data(DummyImage::SmallIcon)), + Some(DummyImage::SmallIcon.get_icon_data()), USER_USER_PAT, ) .await; @@ -294,8 +291,9 @@ async fn add_remove_organization_projects() { #[actix_rt::test] async fn permissions_patch_organization() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { // For each permission covered by EDIT_DETAILS, ensure the permission is required + let api = &test_env.api; let edit_details = OrganizationPermissions::EDIT_DETAILS; let test_pairs = [ ("name", json!("")), // generated in the test to not collide slugs @@ -303,19 +301,22 @@ async fn permissions_patch_organization() { ]; for (key, value) in test_pairs { - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/organization/{}", - ctx.organization_id.unwrap() - )) - .set_json(json!({ - key: if key == "name" { - json!(generate_random_name("randomslug")) - } else { - value.clone() - }, - })) + let req_gen = |ctx: PermissionsTestContext| { + let value = value.clone(); + async move { + api.edit_organization( + &ctx.organization_id.unwrap(), + json!({ + key: if key == "name" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + }), + ctx.test_pat.as_deref(), + ) + .await + } }; PermissionsTest::new(&test_env) .simple_organization_permissions_test(edit_details, req_gen) @@ -329,7 +330,7 @@ async fn permissions_patch_organization() { // Not covered by PATCH /organization #[actix_rt::test] async fn permissions_edit_details() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { let zeta_organization_id = &test_env .dummy .as_ref() @@ -338,19 +339,18 @@ async fn permissions_edit_details() { .organization_id; let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + let api = &test_env.api; let edit_details = OrganizationPermissions::EDIT_DETAILS; // Icon edit // Uses alpha organization to delete this icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/organization/{}/icon?ext=png", - ctx.organization_id.unwrap() - )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization_icon( + &ctx.organization_id.unwrap(), + Some(DummyImage::SmallIcon.get_icon_data()), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -361,11 +361,9 @@ async fn permissions_edit_details() { // Icon delete // Uses alpha project to delete added icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}/icon?ext=png", - ctx.organization_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization_icon(&ctx.organization_id.unwrap(), None, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -394,14 +392,15 @@ async fn permissions_manage_invites() { let manage_invites = OrganizationPermissions::MANAGE_INVITES; // Add member - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) - .set_json(json!({ - "user_id": MOD_USER_ID, - "permissions": 0, - "organization_permissions": 0, - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.add_user_to_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + Some(ProjectPermissions::empty()), + Some(OrganizationPermissions::empty()), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -412,15 +411,16 @@ async fn permissions_manage_invites() { // Edit member let edit_member = OrganizationPermissions::EDIT_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_team_member( + &ctx.team_id.unwrap(), + MOD_USER_ID, + json!({ "organization_permissions": 0, - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -431,11 +431,9 @@ async fn permissions_manage_invites() { // remove member // requires manage_invites if they have not yet accepted the invite - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -454,11 +452,9 @@ async fn permissions_manage_invites() { // remove existing member (requires remove_member) let remove_member = OrganizationPermissions::REMOVE_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) @@ -473,7 +469,7 @@ async fn permissions_manage_invites() { #[actix_rt::test] async fn permissions_add_remove_project() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { let api = &test_env.api; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; @@ -503,15 +499,13 @@ async fn permissions_add_remove_project() { // Now, FRIEND_USER_ID owns the alpha project // Add alpha project to zeta organization - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!( - "/v3/organization/{}/projects", - ctx.organization_id.unwrap() - )) - .set_json(json!({ - "project_id": alpha_project_id, - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.organization_add_project( + &ctx.organization_id.unwrap(), + alpha_project_id, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -522,11 +516,13 @@ async fn permissions_add_remove_project() { // Remove alpha project from zeta organization let remove_project = OrganizationPermissions::REMOVE_PROJECT; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}/projects/{alpha_project_id}", - ctx.organization_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.organization_remove_project( + &ctx.organization_id.unwrap(), + alpha_project_id, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -540,16 +536,15 @@ async fn permissions_add_remove_project() { #[actix_rt::test] async fn permissions_delete_organization() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; let delete_organization = OrganizationPermissions::DELETE_ORGANIZATION; // Now, FRIEND_USER_ID owns the alpha project // Add alpha project to zeta organization - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}", - ctx.organization_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.delete_organization(&ctx.organization_id.unwrap(), ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .simple_organization_permissions_test(delete_organization, req_gen) @@ -570,23 +565,27 @@ async fn permissions_add_default_project_permissions() { .organization_id; let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + let api = &test_env.api; + // Add member let add_member_default_permissions = OrganizationPermissions::MANAGE_INVITES | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; // Failure test should include MANAGE_INVITES, as it is required to add // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS - let failure_with_add_member = (OrganizationPermissions::all() ^ add_member_default_permissions) + let failure_with_add_member = (OrganizationPermissions::all() + ^ add_member_default_permissions) | OrganizationPermissions::MANAGE_INVITES; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) - .set_json(json!({ - "user_id": MOD_USER_ID, - "permissions": (ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION).bits(), - "organization_permissions": 0, - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.add_user_to_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + Some(ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION), + Some(OrganizationPermissions::empty()), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -606,15 +605,16 @@ async fn permissions_add_default_project_permissions() { ^ add_member_default_permissions) | OrganizationPermissions::EDIT_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_team_member( + &ctx.team_id.unwrap(), + MOD_USER_ID, + json!({ "permissions": ProjectPermissions::EDIT_DETAILS.bits(), - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_organization(zeta_organization_id, zeta_team_id) @@ -623,25 +623,26 @@ async fn permissions_add_default_project_permissions() { .simple_organization_permissions_test(modify_member_default_permission, req_gen) .await .unwrap(); - - }).await; + }) + .await; } #[actix_rt::test] async fn permissions_organization_permissions_consistency_test() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; // Ensuring that permission are as we expect them to be // Full organization permissions test let success_permissions = OrganizationPermissions::EDIT_DETAILS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/organization/{}", - ctx.organization_id.unwrap() - )) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_organization( + &ctx.organization_id.unwrap(), + json!({ "description": "Example description - changed.", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .full_organization_permissions_tests(success_permissions, req_gen) diff --git a/tests/pats.rs b/tests/pats.rs index 68dfacad..37e95c26 100644 --- a/tests/pats.rs +++ b/tests/pats.rs @@ -5,6 +5,8 @@ use common::{database::*, environment::with_test_environment_all}; use labrinth::models::pats::Scopes; use serde_json::json; +use crate::common::api_common::AppendsOptionalPat; + mod common; // Full pat test: @@ -20,7 +22,7 @@ pub async fn pat_full_test() { // Create a PAT for a full test let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example "name": "test_pat_scopes Test", @@ -42,7 +44,7 @@ pub async fn pat_full_test() { // Get PAT again let req = test::TestRequest::get() - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .uri("/_internal/pat") .to_request(); let resp = test_env.call(req).await; @@ -76,7 +78,7 @@ pub async fn pat_full_test() { // Change scopes and test again let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": 0, })) @@ -88,7 +90,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!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, "expires": Utc::now() + Duration::seconds(1), // expires in 1 second @@ -104,7 +106,7 @@ pub async fn pat_full_test() { // Change everything back to normal and test again let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "expires": Utc::now() + Duration::days(1), // no longer expired! })) @@ -116,7 +118,7 @@ pub async fn pat_full_test() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past })) @@ -133,7 +135,7 @@ pub async fn pat_full_test() { let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": scope.bits(), })) @@ -147,7 +149,7 @@ pub async fn pat_full_test() { // Delete PAT let req = test::TestRequest::delete() - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .uri(&format!("/_internal/pat/{}", id)) .to_request(); let resp = test_env.call(req).await; @@ -163,7 +165,7 @@ pub async fn bad_pats() { // Creating a PAT with no name should fail let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example "expires": Utc::now() + Duration::days(1), @@ -176,7 +178,7 @@ pub async fn bad_pats() { for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "name": name, "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -190,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("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example "name": "test_pat_scopes Test", @@ -208,7 +210,7 @@ pub async fn bad_pats() { } let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": scope.bits(), "name": format!("test_pat_scopes Name {}", i), @@ -225,7 +227,7 @@ pub async fn bad_pats() { // Create a 'good' PAT for patching let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, "name": "test_pat_scopes Test", @@ -241,7 +243,7 @@ pub async fn bad_pats() { for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() .uri("/_internal/pat") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "name": name, })) @@ -253,7 +255,7 @@ pub async fn bad_pats() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past })) @@ -270,7 +272,7 @@ pub async fn bad_pats() { let req = test::TestRequest::patch() .uri(&format!("/_internal/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "scopes": scope.bits(), })) diff --git a/tests/project.rs b/tests/project.rs index bed7d5c4..0deab73c 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -2,9 +2,7 @@ use std::collections::HashMap; use actix_http::StatusCode; use actix_web::test; -use bytes::Bytes; use chrono::{Duration, Utc}; -use common::api_v3::request_data::get_public_version_creation_data; use common::api_v3::ApiV3; use common::database::*; use common::dummy_data::DUMMY_CATEGORIES; @@ -17,12 +15,13 @@ use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUG use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::projects::{Project, ProjectId}; use labrinth::models::teams::ProjectPermissions; -use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; +use labrinth::util::actix::{MultipartSegment, MultipartSegmentData}; use serde_json::json; +use crate::common::api_common::models::CommonItemType; use crate::common::api_common::request_data::ProjectCreationRequestData; -use crate::common::api_common::{ApiProject, ApiVersion}; -use crate::common::dummy_data::TestFile; +use crate::common::api_common::{ApiProject, ApiTeams, ApiVersion, AppendsOptionalPat}; +use crate::common::dummy_data::{DummyImage, TestFile}; mod common; #[actix_rt::test] @@ -67,7 +66,7 @@ async fn test_get_project() { // Make the request again, this time it should be cached let req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = test_env.call(req).await; let status = resp.status(); @@ -80,7 +79,7 @@ async fn test_get_project() { // Request should fail on non-existent project let req = test::TestRequest::get() .uri("/v3/project/nonexistent") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = test_env.call(req).await; @@ -89,7 +88,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!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", ENEMY_USER_PAT)) + .append_pat(ENEMY_USER_PAT) .to_request(); let resp = test_env.call(req).await; @@ -132,35 +131,35 @@ async fn test_add_remove_project() { ..json_segment.clone() }; + let basic_mod_file = TestFile::BasicMod; + let basic_mod_different_file = TestFile::BasicModDifferent; + // 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 used in the reuse data - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod.jar").to_vec(), - ), + // 'Basic' + name: basic_mod_file.filename(), + filename: Some(basic_mod_file.filename()), + content_type: basic_mod_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), }; - // Differently named file, with the same content (for hash testing) + // 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(), - ), + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + // 'Basic' + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), }; // 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(), - ), + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_different_file.bytes()), }; // Add a project- simple, should work. @@ -198,7 +197,7 @@ async fn test_add_remove_project() { let uploaded_version_id = versions[0].id; // Checks files to ensure they were uploaded and correctly identify the file - let hash = sha1::Sha1::from(include_bytes!("../tests/files/basic-mod.jar")) + let hash = sha1::Sha1::from(basic_mod_file.bytes()) .digest() .to_string(); let version = api @@ -597,12 +596,101 @@ pub async fn test_bulk_edit_links() { .await; } +#[actix_rt::test] +async fn delete_project_with_report() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + 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; + + // Create a report for the project + let resp = api + .create_report( + "copyright", + alpha_project_id, + CommonItemType::Project, + "Hey! This is my project, copied without permission!", + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let value = test::read_body_json::(resp).await; + let alpha_report_id = value["id"].as_str().unwrap(); + + // Confirm existence + let resp = api + .get_report( + alpha_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + // Do the same for beta + let resp = api + .create_report( + "copyright", + beta_project_id, + CommonItemType::Project, + "Hey! This is my project, copied without permission!", + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let value = test::read_body_json::(resp).await; + let beta_report_id = value["id"].as_str().unwrap(); + + // Delete the project + let resp = api.remove_project(alpha_project_id, USER_USER_PAT).await; + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + + // Confirm that the project is gone from the cache + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, alpha_project_id) + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + + // Report for alpha no longer exists + let resp = api + .get_report( + alpha_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // Confirm that report for beta still exists + let resp = api + .get_report( + beta_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + }) + .await; +} + #[actix_rt::test] async fn permissions_patch_project_v3() { with_test_environment(Some(8), |test_env: TestEnvironment| async move { 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; + let api = &test_env.api; // TODO: This should be a separate test from v3 // - only a couple of these fields are v3-specific // once we have permissions/scope tests setup to not just take closures, we can split this up @@ -630,16 +718,22 @@ async fn permissions_patch_project_v3() { .map(|(key, value)| { let test_env = test_env.clone(); async move { - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - key: if key == "slug" { - json!(generate_random_name("randomslug")) - } else { - value.clone() - }, - })) + let req_gen = |ctx: PermissionsTestContext| { + let value = value.clone(); + async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ + key: if key == "slug" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + }), + ctx.test_pat.as_deref(), + ) + .await + } }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_details, req_gen) @@ -653,13 +747,16 @@ async fn permissions_patch_project_v3() { // Test with status and requested_status // This requires a project with a version, so we use alpha_project_id - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ "status": "private", "requested_status": "private", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -669,15 +766,15 @@ async fn permissions_patch_project_v3() { .unwrap(); // Bulk patch projects - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/projects?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{}\"", ctx.project_id.unwrap())) - )) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project_bulk( + &[&ctx.project_id.unwrap()], + json!({ "name": "randomname", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_details, req_gen) @@ -687,12 +784,15 @@ async fn permissions_patch_project_v3() { // Edit body // Cannot bulk edit body let edit_body = ProjectPermissions::EDIT_BODY; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ "description": "new description!", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_body, req_gen) @@ -711,12 +811,14 @@ async fn permissions_edit_details() { let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; let beta_team_id = &test_env.dummy.as_ref().unwrap().project_beta.team_id; let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + let edit_details = ProjectPermissions::EDIT_DETAILS; + let api = &test_env.api; // Approve beta version as private so we can schedule it let req = test::TestRequest::patch() .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .set_json(json!({ "status": "unlisted" })) @@ -725,15 +827,14 @@ async fn permissions_edit_details() { assert_eq!(resp.status(), 204); // Schedule version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::post() - .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", - "time": Utc::now() + Duration::days(1), - } - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.schedule_project( + beta_version_id, + "archived", + Utc::now() + Duration::days(1), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(beta_project_id, beta_team_id) @@ -744,15 +845,13 @@ async fn permissions_edit_details() { // Icon edit // Uses alpha project to delete this icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/project/{}/icon?ext=png", - ctx.project_id.unwrap() - )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project_icon( + &ctx.project_id.unwrap(), + Some(DummyImage::SmallIcon.get_icon_data()), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -763,11 +862,9 @@ async fn permissions_edit_details() { // Icon delete // Uses alpha project to delete added icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/project/{}/icon?ext=png", - ctx.project_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project_icon(&ctx.project_id.unwrap(), None, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -778,15 +875,17 @@ async fn permissions_edit_details() { // Add gallery item // Uses alpha project to add gallery item so we can get its url - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!( - "/v3/project/{}/gallery?ext=png&featured=true", - ctx.project_id.unwrap() - )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.add_gallery_item( + &ctx.project_id.unwrap(), + DummyImage::SmallIcon.get_icon_data(), + true, + None, + None, + None, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -797,7 +896,7 @@ async fn permissions_edit_details() { // Get project, as we need the gallery image url let req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = test_env.call(req).await; let project: serde_json::Value = test::read_body_json(resp).await; @@ -805,11 +904,16 @@ async fn permissions_edit_details() { // Edit gallery item // Uses alpha project to edit gallery item - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch().uri(&format!( - "/v3/project/{}/gallery?url={gallery_url}", - ctx.project_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_gallery_item( + &ctx.project_id.unwrap(), + gallery_url, + vec![("description".to_string(), "new caption!".to_string())] + .into_iter() + .collect(), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -820,11 +924,13 @@ async fn permissions_edit_details() { // Remove gallery item // Uses alpha project to remove gallery item - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/project/{}/gallery?url={gallery_url}", - ctx.project_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_gallery_item( + &ctx.project_id.unwrap(), + gallery_url, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -844,21 +950,22 @@ async fn permissions_upload_version() { 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 api = &test_env.api; + let upload_version = ProjectPermissions::UPLOAD_VERSION; // Upload version with basic-mod.jar - let req_gen = |ctx: &PermissionsTestContext| { + let req_gen = |ctx: PermissionsTestContext| async move { let project_id = ctx.project_id.unwrap(); - let project_id = ProjectId(parse_base62(project_id).unwrap()); - let multipart = get_public_version_creation_data( + let project_id = ProjectId(parse_base62(&project_id).unwrap()); + api.add_public_version( project_id, "1.0.0", TestFile::BasicMod, None, None, - ); - test::TestRequest::post() - .uri("/v3/version") - .set_multipart(multipart.segment_data) + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(upload_version, req_gen) @@ -867,30 +974,13 @@ async fn permissions_upload_version() { // Upload file to existing version // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/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(), - ), - }, - ]) + let req_gen = |ctx: PermissionsTestContext| async move { + api.upload_file_to_version( + alpha_version_id, + &TestFile::BasicModDifferent, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -901,12 +991,15 @@ async fn permissions_upload_version() { // Patch version // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/version/{}", alpha_version_id)) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_version( + alpha_version_id, + json!({ "name": "Basic Mod", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -918,8 +1011,9 @@ async fn permissions_upload_version() { // 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!("/v3/version_file/{}", alpha_file_hash)) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version_file(alpha_file_hash, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) @@ -931,8 +1025,9 @@ 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!("/v3/version/{}", alpha_version_id)) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -951,16 +1046,19 @@ async fn permissions_manage_invites() { 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; + let api = &test_env.api; let manage_invites = ProjectPermissions::MANAGE_INVITES; // Add member - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) - .set_json(json!({ - "user_id": MOD_USER_ID, - "permissions": 0, - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.add_user_to_team( + &ctx.team_id.unwrap(), + MOD_USER_ID, + Some(ProjectPermissions::empty()), + None, + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -971,15 +1069,16 @@ async fn permissions_manage_invites() { // Edit member let edit_member = ProjectPermissions::EDIT_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_team_member( + &ctx.team_id.unwrap(), + MOD_USER_ID, + json!({ "permissions": 0, - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -990,11 +1089,9 @@ async fn permissions_manage_invites() { // remove member // requires manage_invites if they have not yet accepted the invite - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -1006,7 +1103,7 @@ async fn permissions_manage_invites() { // re-add member for testing let req = test::TestRequest::post() .uri(&format!("/v3/team/{}/members", alpha_team_id)) - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .set_json(json!({ "user_id": MOD_USER_ID, })) @@ -1017,18 +1114,16 @@ async fn permissions_manage_invites() { // Accept invite let req = test::TestRequest::post() .uri(&format!("/v3/team/{}/join", alpha_team_id)) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); // remove existing member (requires remove_member) let remove_member = ProjectPermissions::REMOVE_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_from_team(&ctx.team_id.unwrap(), MOD_USER_ID, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) @@ -1046,10 +1141,11 @@ async fn permissions_delete_project() { // Add member, remove member, edit member with_test_environment_all(None, |test_env| async move { let delete_project = ProjectPermissions::DELETE_PROJECT; - + let api = &test_env.api; // Delete project - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_project(&ctx.project_id.unwrap(), ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(delete_project, req_gen) @@ -1066,15 +1162,18 @@ async fn project_permissions_consistency_test() { with_test_environment_all(Some(10), |test_env| async move { // 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 - + let api = &test_env.api; // Full project permissions test with EDIT_DETAILS let success_permissions = ProjectPermissions::EDIT_DETAILS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "name": "Example title - changed.", - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ + "categories": [], + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .full_project_permissions_test(success_permissions, req_gen) @@ -1086,12 +1185,15 @@ async fn project_permissions_consistency_test() { | ProjectPermissions::REMOVE_MEMBER | ProjectPermissions::DELETE_VERSION | ProjectPermissions::VIEW_PAYOUTS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "name": "Example title - changed.", - })) + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ + "categories": [], + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .full_project_permissions_test(success_permissions, req_gen) @@ -1112,7 +1214,7 @@ async fn align_search_projects() { let projects = api .search_deserialized( - Some(&test_name), + Some(&format!("\"&{test_name}\"")), Some(json!([["categories:fabric"]])), USER_USER_PAT, ) @@ -1122,6 +1224,7 @@ async fn align_search_projects() { let project_model = api .get_project(&project.id.to_string(), USER_USER_PAT) .await; + assert_eq!(project_model.status(), 200); let mut project_model: Project = test::read_body_json(project_model).await; // Body/description is huge- don't store it in search, so it's OK if they differ here diff --git a/tests/scopes.rs b/tests/scopes.rs index 335621ce..0aa53ce5 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -14,7 +14,8 @@ use labrinth::models::projects::ProjectId; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; -use crate::common::api_common::ApiTeams; +use crate::common::api_common::{ApiTeams, AppendsOptionalPat}; +use crate::common::dummy_data::DummyImage; // For each scope, we (using test_scope): // - create a PAT with a given set of scopes for a function @@ -386,7 +387,7 @@ pub async fn project_version_reads_scopes() { let read_version = Scopes::VERSION_READ; let req = test::TestRequest::patch() .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "status": "draft" })) @@ -505,6 +506,8 @@ pub async fn project_write_scopes() { .team_id .clone(); + let test_icon = DummyImage::SmallIcon; + // Projects writing let write_project = Scopes::PROJECT_WRITE; let req_gen = || { @@ -541,7 +544,7 @@ pub async fn project_write_scopes() { // Approve beta as private so we can schedule it let req = test::TestRequest::patch() .uri(&format!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .set_json(json!({ "status": "private" })) @@ -567,10 +570,11 @@ pub async fn project_write_scopes() { // Icons and gallery images let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v3/project/{beta_project_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + .uri(&format!( + "/v3/project/{beta_project_id}/icon?ext={ext}", + ext = test_icon.extension() )) + .set_payload(test_icon.bytes()) }; ScopeTest::new(&test_env) .test(req_gen, write_project) @@ -587,11 +591,10 @@ pub async fn project_write_scopes() { let req_gen = || { test::TestRequest::post() .uri(&format!( - "/v3/project/{beta_project_id}/gallery?ext=png&featured=true" - )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + "/v3/project/{beta_project_id}/gallery?ext={ext}&featured=true", + ext = test_icon.extension() )) + .set_payload(test_icon.bytes()) }; ScopeTest::new(&test_env) .test(req_gen, write_project) @@ -601,7 +604,7 @@ pub async fn project_write_scopes() { // Get project, as we need the gallery image url let req_gen = test::TestRequest::get() .uri(&format!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = test_env.call(req_gen).await; let project: serde_json::Value = test::read_body_json(resp).await; @@ -728,12 +731,14 @@ pub async fn version_write_scopes() { .file_hash .clone(); + let basic_zip = TestFile::BasicZip; + let write_version = Scopes::VERSION_WRITE; // Approve beta version as private so we can schedule it let req = test::TestRequest::patch() .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", MOD_USER_PAT)) + .append_pat(MOD_USER_PAT) .set_json(json!({ "status": "unlisted" })) @@ -782,7 +787,7 @@ pub async fn version_write_scopes() { serde_json::to_string(&json!( { "file_types": { - "simple-zip.zip": "required-resource-pack" + basic_zip.filename(): "required-resource-pack" }, } )) @@ -792,12 +797,10 @@ pub async fn version_write_scopes() { // Differently named file, with different content let content_segment = MultipartSegment { - name: "simple-zip.zip".to_string(), - filename: Some("simple-zip.zip".to_string()), - content_type: Some("application/zip".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/simple-zip.zip").to_vec(), - ), + name: basic_zip.filename(), + filename: Some(basic_zip.filename()), + content_type: basic_zip.content_type(), + data: MultipartSegmentData::Binary(basic_zip.bytes()), }; // Upload version file @@ -995,7 +998,7 @@ pub async fn thread_scopes() { // First, get message id let req_gen = test::TestRequest::get() .uri(&format!("/v3/thread/{thread_id}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .to_request(); let resp = test_env.call(req_gen).await; let success: serde_json::Value = test::read_body_json(resp).await; @@ -1077,6 +1080,8 @@ pub async fn collections_scopes() { .project_id .clone(); + let small_icon = DummyImage::SmallIcon; + // Create collection let collection_create = Scopes::COLLECTION_CREATE; let req_gen = || { @@ -1145,10 +1150,11 @@ pub async fn collections_scopes() { let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v3/collection/{collection_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + .uri(&format!( + "/v3/collection/{collection_id}/icon?ext={ext}", + ext = small_icon.extension() )) + .set_payload(Bytes::from(small_icon.bytes())) }; ScopeTest::new(&test_env) .test(req_gen, collection_write) @@ -1178,6 +1184,8 @@ pub async fn organization_scopes() { .project_id .clone(); + let icon = DummyImage::SmallIcon; + // Create organization let organization_create = Scopes::ORGANIZATION_CREATE; let req_gen = || { @@ -1210,10 +1218,11 @@ pub async fn organization_scopes() { let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v3/organization/{organization_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + .uri(&format!( + "/v3/organization/{organization_id}/icon?ext={ext}", + ext = icon.extension() )) + .set_payload(Bytes::from(icon.bytes())) }; ScopeTest::new(&test_env) .test(req_gen, organization_edit) diff --git a/tests/search.rs b/tests/search.rs index aabacd5b..111dde8a 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -91,7 +91,11 @@ async fn search_projects() { let test_name = test_name.clone(); async move { let projects = api - .search_deserialized(Some(&test_name), Some(facets.clone()), USER_USER_PAT) + .search_deserialized( + Some(&format!("\"&{test_name}\"")), + Some(facets.clone()), + USER_USER_PAT, + ) .await; let mut found_project_ids: Vec = projects .hits diff --git a/tests/teams.rs b/tests/teams.rs index c4bf7cda..b25044e8 100644 --- a/tests/teams.rs +++ b/tests/teams.rs @@ -1,4 +1,7 @@ -use crate::common::{api_common::ApiTeams, database::*}; +use crate::common::{ + api_common::{ApiTeams, AppendsOptionalPat}, + database::*, +}; use actix_web::test; use common::{ api_v3::ApiV3, @@ -36,7 +39,7 @@ async fn test_get_team() { ] { let req = test::TestRequest::get() .uri(&uri) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; @@ -51,7 +54,7 @@ async fn test_get_team() { // - should not appear in the team members list to enemy users let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(&json!({ "user_id": FRIEND_USER_ID, })) @@ -65,7 +68,7 @@ async fn test_get_team() { ] { let req = test::TestRequest::get() .uri(&uri) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); @@ -87,7 +90,7 @@ async fn test_get_team() { let req = test::TestRequest::get() .uri(&uri) - .append_header(("Authorization", ENEMY_USER_PAT)) + .append_pat(ENEMY_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); @@ -101,7 +104,7 @@ async fn test_get_team() { // and should be able to see private data about the team let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); @@ -112,7 +115,7 @@ async fn test_get_team() { ] { let req = test::TestRequest::get() .uri(&uri) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); @@ -154,7 +157,7 @@ async fn test_get_team_project_orgs() { // Attach alpha to zeta let req = test::TestRequest::post() .uri(&format!("/v3/organization/{zeta_organization_id}/projects")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "project_id": alpha_project_id, })) @@ -165,7 +168,7 @@ async fn test_get_team_project_orgs() { // Invite and add friend to zeta let req = test::TestRequest::post() .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "user_id": FRIEND_USER_ID, })) @@ -175,7 +178,7 @@ async fn test_get_team_project_orgs() { let req = test::TestRequest::post() .uri(&format!("/v3/team/{zeta_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); @@ -185,7 +188,7 @@ async fn test_get_team_project_orgs() { // - not the ones from the organization let req = test::TestRequest::get() .uri(&format!("/v3/team/{alpha_team_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); @@ -197,7 +200,7 @@ async fn test_get_team_project_orgs() { // - the members of the project team including the ones from the organization let req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); @@ -290,7 +293,7 @@ async fn test_patch_organization_team_member() { let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) .set_json(json!({})) - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); @@ -298,7 +301,7 @@ async fn test_patch_organization_team_member() { // As a non-owner with full permissions, attempt to edit the owner's permissions let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", ADMIN_USER_PAT)) + .append_pat(ADMIN_USER_PAT) .set_json(json!({ "permissions": 0 })) @@ -312,7 +315,7 @@ async fn test_patch_organization_team_member() { // first, invite friend let req = test::TestRequest::post() .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "user_id": FRIEND_USER_ID, "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS).bits(), @@ -323,7 +326,7 @@ async fn test_patch_organization_team_member() { // accept let req = test::TestRequest::post() .uri(&format!("/v3/team/{zeta_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); @@ -331,7 +334,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!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .set_json(json!({ "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() })) @@ -344,7 +347,7 @@ async fn test_patch_organization_team_member() { for payout in [-1, 5001] { let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_json(json!({ "payouts_split": payout })) @@ -356,7 +359,7 @@ async fn test_patch_organization_team_member() { // Successful patch let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .set_json(json!({ "payouts_split": 51, "organization_permissions": (OrganizationPermissions::EDIT_MEMBER).bits(), // reduces permissions @@ -372,7 +375,7 @@ async fn test_patch_organization_team_member() { // Check results let req = test::TestRequest::get() .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) + .append_pat(FRIEND_USER_PAT) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); diff --git a/tests/v2/project.rs b/tests/v2/project.rs index ad2ee21f..ba03ce60 100644 --- a/tests/v2/project.rs +++ b/tests/v2/project.rs @@ -1,9 +1,8 @@ +use std::sync::Arc; + use crate::common::{ - api_common::ApiProject, - api_v2::{ - request_data::{get_public_project_creation_data_json, get_public_version_creation_data}, - ApiV2, - }, + api_common::{ApiProject, ApiVersion, AppendsOptionalPat}, + api_v2::{request_data::get_public_project_creation_data_json, ApiV2}, database::{ generate_random_name, ADMIN_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT, }, @@ -96,41 +95,41 @@ async fn test_add_remove_project() { ..json_segment.clone() }; + let basic_mod_file = TestFile::BasicMod; + let basic_mod_different_file = TestFile::BasicModDifferent; + // 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(), - ), + // 'Basic' + name: basic_mod_file.filename(), + filename: Some(basic_mod_file.filename()), + content_type: basic_mod_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), }; - // Differently named file, with the same content (for hash testing) + // 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(), - ), + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + // 'Basic' + data: MultipartSegmentData::Binary(basic_mod_file.bytes()), }; // 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(), - ), + // 'Different' + name: basic_mod_different_file.filename(), + filename: Some(basic_mod_different_file.filename()), + content_type: basic_mod_different_file.content_type(), + data: MultipartSegmentData::Binary(basic_mod_different_file.bytes()), }; // Add a project- simple, should work. let req = test::TestRequest::post() .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(USER_USER_PAT) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); let resp = test_env.call(req).await; @@ -144,7 +143,7 @@ async fn test_add_remove_project() { 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")) + let hash = sha1::Sha1::from(basic_mod_file.bytes()) .digest() .to_string(); let version = api @@ -156,7 +155,7 @@ async fn test_add_remove_project() { // Even if that file is named differently let req = test::TestRequest::post() .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(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 @@ -169,7 +168,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") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(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 @@ -182,7 +181,7 @@ async fn test_add_remove_project() { // Different slug, different file should succeed let req = test::TestRequest::post() .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) + .append_pat(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 @@ -234,23 +233,24 @@ async fn permissions_upload_version() { 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 api = &test_env.api; + let basic_mod_different_file = TestFile::BasicModDifferent; let upload_version = ProjectPermissions::UPLOAD_VERSION; - // Upload version with basic-mod.jar - let req_gen = |ctx: &PermissionsTestContext| { + let req_gen = |ctx: PermissionsTestContext| async move { let project_id = ctx.project_id.unwrap(); - let project_id = ProjectId(parse_base62(project_id).unwrap()); - let multipart = get_public_version_creation_data( + let project_id = ProjectId(parse_base62(&project_id).unwrap()); + api.add_public_version( project_id, "1.0.0", TestFile::BasicMod, None, None, - ); - test::TestRequest::post() - .uri("/v2/version") - .set_multipart(multipart.segment_data) + ctx.test_pat.as_deref(), + ) + .await }; + PermissionsTest::new(&test_env) .simple_project_permissions_test(upload_version, req_gen) .await @@ -258,30 +258,13 @@ async fn permissions_upload_version() { // 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(), - ), - }, - ]) + let file_ref = Arc::new(basic_mod_different_file); + let req_gen = |ctx: PermissionsTestContext| { + let file_ref = file_ref.clone(); + async move { + api.upload_file_to_version(alpha_version_id, &file_ref, ctx.test_pat.as_deref()) + .await + } }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -292,13 +275,17 @@ async fn permissions_upload_version() { // 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!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_version( + alpha_version_id, + json!({ "name": "Basic Mod", - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; + PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) @@ -309,8 +296,9 @@ async fn permissions_upload_version() { // 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)) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version_file(alpha_file_hash, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) @@ -322,8 +310,9 @@ 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)) + let req_gen = |ctx: PermissionsTestContext| async move { + api.remove_version(alpha_version_id, ctx.test_pat.as_deref()) + .await }; PermissionsTest::new(&test_env) .with_existing_project(alpha_project_id, alpha_team_id) @@ -374,6 +363,8 @@ pub async fn test_patch_v2() { #[actix_rt::test] async fn permissions_patch_project_v2() { with_test_environment(Some(8), |test_env: TestEnvironment| async move { + let api = &test_env.api; + // TODO: This only includes v2 ones (as it should. See v3) // For each permission covered by EDIT_DETAILS, ensure the permission is required let edit_details = ProjectPermissions::EDIT_DETAILS; @@ -397,16 +388,19 @@ async fn permissions_patch_project_v2() { .map(|(key, value)| { let test_env = test_env.clone(); async move { - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ key: if key == "slug" { json!(generate_random_name("randomslug")) } else { value.clone() }, - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_details, req_gen) @@ -421,12 +415,15 @@ async fn permissions_patch_project_v2() { // Edit body // Cannot bulk edit body let edit_body = ProjectPermissions::EDIT_BODY; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v2/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ + let req_gen = |ctx: PermissionsTestContext| async move { + api.edit_project( + &ctx.project_id.unwrap(), + json!({ "body": "new body!", // new body - })) + }), + ctx.test_pat.as_deref(), + ) + .await }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_body, req_gen) diff --git a/tests/v2/search.rs b/tests/v2/search.rs index 1598ad3e..94ac908b 100644 --- a/tests/v2/search.rs +++ b/tests/v2/search.rs @@ -28,7 +28,7 @@ async fn search_projects() { let create_async_future = |id: u64, - pat: &'static str, + pat: Option<&'static str>, is_modpack: bool, modify_json: Option| { let slug = format!("{test_name}-searchable-project-{id}"); @@ -292,7 +292,11 @@ async fn search_projects() { let test_name = test_name.clone(); async move { let projects = api - .search_deserialized(Some(&test_name), Some(facets.clone()), USER_USER_PAT) + .search_deserialized( + Some(&format!("\"&{test_name}\"")), + Some(facets.clone()), + USER_USER_PAT, + ) .await; let mut found_project_ids: Vec = projects .hits @@ -309,7 +313,7 @@ async fn search_projects() { // A couple additional tests for the saerch type returned, making sure it is properly translated back let client_side_required = api .search_deserialized( - Some(&test_name), + Some(&format!("\"&{test_name}\"")), Some(json!([["client_side:required"]])), USER_USER_PAT, ) @@ -320,7 +324,7 @@ async fn search_projects() { let server_side_required = api .search_deserialized( - Some(&test_name), + Some(&format!("\"&{test_name}\"")), Some(json!([["server_side:required"]])), USER_USER_PAT, ) @@ -331,7 +335,7 @@ async fn search_projects() { let client_side_unsupported = api .search_deserialized( - Some(&test_name), + Some(&format!("\"&{test_name}\"")), Some(json!([["client_side:unsupported"]])), USER_USER_PAT, ) @@ -342,7 +346,7 @@ async fn search_projects() { let game_versions = api .search_deserialized( - Some(&test_name), + Some(&format!("\"&{test_name}\"")), Some(json!([["versions:1.20.5"]])), USER_USER_PAT, ) diff --git a/tests/v2/tags.rs b/tests/v2/tags.rs index cb072ba0..9b854337 100644 --- a/tests/v2/tags.rs +++ b/tests/v2/tags.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use labrinth::routes::v2::tags::DonationPlatformQueryData; use std::collections::HashSet; @@ -62,3 +63,45 @@ async fn get_tags() { }) .await; } + +#[actix_rt::test] +async fn get_donation_platforms() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let mut donation_platforms_unsorted = api.get_donation_platforms_deserialized().await; + + // These tests match dummy data and will need to be updated if the dummy data changes + let mut included = vec![ + DonationPlatformQueryData { + short: "patreon".to_string(), + name: "Patreon".to_string(), + }, + DonationPlatformQueryData { + short: "ko-fi".to_string(), + name: "Ko-fi".to_string(), + }, + DonationPlatformQueryData { + short: "paypal".to_string(), + name: "PayPal".to_string(), + }, + DonationPlatformQueryData { + short: "bmac".to_string(), + name: "Buy Me A Coffee".to_string(), + }, + DonationPlatformQueryData { + short: "github".to_string(), + name: "GitHub Sponsors".to_string(), + }, + DonationPlatformQueryData { + short: "other".to_string(), + name: "Other".to_string(), + }, + ]; + + included.sort_by(|a, b| a.short.cmp(&b.short)); + donation_platforms_unsorted.sort_by(|a, b| a.short.cmp(&b.short)); + + assert_eq!(donation_platforms_unsorted, included); + }) + .await; +}