mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
feat: add testing setup in backend
This commit is contained in:
1 parent
cb376caff0
commit
1f77328362
15 files changed
+92
-100
No files matched your search
@@ -1,14 +1,17 @@
|
||||
version: '3'
|
||||
services:
|
||||
cypress:
|
||||
image: cypress/included
|
||||
build:
|
||||
context: .
|
||||
dockerfile: tests/Dockerfile
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- ./tests:/e2e
|
||||
working_dir: /e2e
|
||||
- ./tests:/e2e/tests
|
||||
environment:
|
||||
- BASE_URL=http://app-test
|
||||
- TESTING_USERNAME=alex
|
||||
- TESTING_PASSWORD=Alex1234
|
||||
depends_on:
|
||||
- app-test
|
||||
|
||||
@@ -49,6 +52,9 @@ services:
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
||||
- SURVEY_URL=${SURVEY_URL}
|
||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
||||
- TESTING=true
|
||||
- TESTING_USERNAME=alex
|
||||
- TESTING_PASSWORD=Alex1234
|
||||
|
||||
db-test:
|
||||
image: mongo
|
||||
|
||||
@@ -45,6 +45,9 @@ services:
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
||||
- TESTING=true
|
||||
- TESTING_USERNAME=alex
|
||||
- TESTING_PASSWORD=Alex1234
|
||||
|
||||
analytics:
|
||||
build:
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"format": "prettier --ignore-path .prettierignore --write app server",
|
||||
"test": "yarn node --experimental-vm-modules $(yarn bin jest)",
|
||||
"test:e2e": "yarn workspace tests test",
|
||||
"test:open": "yarn workspace tests test:open",
|
||||
"analytics": "yarn workspace analytics dev",
|
||||
"setup": "yarn workspace server setup",
|
||||
"clean": "yarn workspace server clean"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { matchedData } from 'express-validator'
|
||||
import { User } from '@linux-odyssey/models'
|
||||
import { genJWT, hashPassword } from '../../utils/auth.js'
|
||||
import { genJWT } from '../../utils/auth.js'
|
||||
import { createUser } from '../../models/userManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
export const issueToken = asyncHandler(async (req, res) => {
|
||||
@@ -16,12 +17,7 @@ export const issueToken = asyncHandler(async (req, res) => {
|
||||
export const register = asyncHandler(async (req, res, next) => {
|
||||
const { username, password, email } = matchedData(req)
|
||||
|
||||
const user = new User({
|
||||
username,
|
||||
email,
|
||||
})
|
||||
user.hashedPassword = await hashPassword(password)
|
||||
await user.save()
|
||||
const user = await createUser(username, email, password)
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Command, Session } from '@linux-odyssey/models'
|
||||
import { matchedData } from 'express-validator'
|
||||
import CommandHandler from '../../game/commandHandler.js'
|
||||
import { pushToSession } from '../socket.js'
|
||||
import { finishSession } from '../../game/sessionManager.js'
|
||||
import { finishSession } from '../../models/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
const commandCompleteCallbacks = new Map()
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createNewSession,
|
||||
getOrCreateActiveSession,
|
||||
isQuestUnlocked,
|
||||
} from '../../game/sessionManager.js'
|
||||
} from '../../models/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
|
||||
function sessionSummary(session) {
|
||||
|
||||
@@ -40,6 +40,11 @@ const config = {
|
||||
trustedProxies: getTrustProxies('TRUSTED_PROXIES'),
|
||||
surveyUrl: getUrl('SURVEY_URL', ''),
|
||||
bugReportUrl: getUrl('BUG_REPORT_URL', ''),
|
||||
testing: {
|
||||
enabled: process.env.TESTING === 'true',
|
||||
username: get('TESTING_USERNAME', ''),
|
||||
password: get('TESTING_PASSWORD', ''),
|
||||
},
|
||||
}
|
||||
|
||||
config.baseUrl = get('BASE_URL', `http://${config.host}:${config.port}`)
|
||||
|
||||
@@ -15,6 +15,7 @@ import errorHandler from './middleware/error.js'
|
||||
import { globalRateLimit } from './middleware/rateLimiter.js'
|
||||
import sessionMiddleware from './middleware/session.js'
|
||||
import expiryRemovalScheduler from './containers/expiryChecker.js'
|
||||
import setupTest from './utils/test.js'
|
||||
|
||||
async function main() {
|
||||
if (!config.secret) {
|
||||
@@ -33,6 +34,10 @@ async function main() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (config.testing.enabled) {
|
||||
await setupTest()
|
||||
}
|
||||
|
||||
await loadAndUpdateQuests()
|
||||
expiryRemovalScheduler()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Session, Quest, UserProfile } from '@linux-odyssey/models'
|
||||
import { createContainer, deleteContainer } from '../containers/docker.js'
|
||||
import SessionHandler from './sessionHandler.js'
|
||||
import SessionHandler from '../game/sessionHandler.js'
|
||||
|
||||
async function deactivateSessions(user, quest) {
|
||||
const sessions = await Session.find({
|
||||
@@ -0,0 +1,16 @@
|
||||
import { User, UserProfile } from '@linux-odyssey/models'
|
||||
import { hashPassword } from '../utils/auth.js'
|
||||
|
||||
export async function createUser(username, email, password) {
|
||||
const user = new User({
|
||||
username,
|
||||
email,
|
||||
})
|
||||
user.hashedPassword = await hashPassword(password)
|
||||
await user.save()
|
||||
|
||||
const userProfile = new UserProfile({
|
||||
user,
|
||||
})
|
||||
await userProfile.save()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { User } from '@linux-odyssey/models'
|
||||
import config from '../config.js'
|
||||
import { createUser } from '../models/userManager.js'
|
||||
|
||||
export default async function setupTest() {
|
||||
console.log('Setting up test account')
|
||||
if (!(config.testing.username && config.testing.password)) {
|
||||
throw new Error(
|
||||
'No testing account found! Please set TESTING_USERNAME and TESTING_PASSWORD in .env'
|
||||
)
|
||||
}
|
||||
const { username, password } = config.testing
|
||||
const email = `${username}@example.com`
|
||||
|
||||
if (await User.exists({ username })) {
|
||||
console.log('Testing account already exists')
|
||||
return
|
||||
}
|
||||
|
||||
await createUser(username, email, password)
|
||||
console.log('Created testing account')
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM cypress/included:13.5.1
|
||||
|
||||
WORKDIR /e2e
|
||||
COPY package.json yarn.lock ./
|
||||
COPY tests/package.json ./tests/package.json
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY tests ./tests
|
||||
WORKDIR /e2e/tests
|
||||
+11
-4
@@ -1,11 +1,19 @@
|
||||
const { defineConfig } = require('cypress')
|
||||
const process = require('process')
|
||||
require('dotenv').config()
|
||||
require('dotenv').config({ path: '../.env' })
|
||||
|
||||
const getConfig = (key, defaultValue) => {
|
||||
return process.env[key] || defaultValue
|
||||
}
|
||||
|
||||
const getOrFail = (key) => {
|
||||
const value = getConfig(key)
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable ${key}`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
baseUrl: getConfig('BASE_URL', 'http://localhost:5173'),
|
||||
@@ -14,9 +22,8 @@ module.exports = defineConfig({
|
||||
pageLoadTimeout: 120000,
|
||||
},
|
||||
env: {
|
||||
...process.env,
|
||||
defaultAccount: process.env.default_USERNAME,
|
||||
defaultPassword: process.env.default_PASSWORD,
|
||||
defaultAccount: getOrFail('TESTING_USERNAME'),
|
||||
defaultPassword: getOrFail('TESTING_PASSWORD'),
|
||||
// google_username: process.env.GOOGLE_USERNAME,
|
||||
// google_password: process.env.GOOGLE_PASSWORD,
|
||||
// google_client_id: process.env.GOOGLE_CLIENT_ID,
|
||||
|
||||
+4
-7
@@ -5,15 +5,12 @@
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "cypress run"
|
||||
"test": "cypress run",
|
||||
"test:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"cypress": "^13.2.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"save": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^13.5.0",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"cypress": "^13.5.0"
|
||||
"dotenv": "^16.3.1"
|
||||
}
|
||||
}
|
||||
@@ -1467,11 +1467,6 @@ async@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
|
||||
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
|
||||
|
||||
async@^3.2.2:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
|
||||
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -2345,11 +2340,6 @@ dotenv@^16.3.1:
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
|
||||
|
||||
duplexer@^0.1.1, duplexer@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
|
||||
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
@@ -2786,19 +2776,6 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
event-stream@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65"
|
||||
integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==
|
||||
dependencies:
|
||||
duplexer "^0.1.1"
|
||||
from "^0.1.7"
|
||||
map-stream "0.0.7"
|
||||
pause-stream "^0.0.11"
|
||||
split "^1.0.1"
|
||||
stream-combiner "^0.2.2"
|
||||
through "^2.3.8"
|
||||
|
||||
eventemitter2@6.4.7:
|
||||
version "6.4.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
|
||||
@@ -3145,11 +3122,6 @@ fresh@0.5.2:
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
|
||||
from@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
|
||||
integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
@@ -4470,11 +4442,6 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.assign@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||
integrity sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
@@ -4593,11 +4560,6 @@ makeerror@1.0.12:
|
||||
dependencies:
|
||||
tmpl "1.0.5"
|
||||
|
||||
map-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
|
||||
integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
@@ -4663,11 +4625,6 @@ mimic-fn@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
||||
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
|
||||
|
||||
mingo@^6.1.0:
|
||||
version "6.4.9"
|
||||
resolved "https://registry.yarnpkg.com/mingo/-/mingo-6.4.9.tgz#715ad1487687259db04623b64a7ba63b564b5ddd"
|
||||
integrity sha512-jsOiIEJ19UtTaWjNFN4CnuKWqELm2sINZcHqI/nmjezMvXthBIQOftJrlT85NuIDoSFc0vrbjD+GnrzEgYnPbA==
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
@@ -5229,13 +5186,6 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
|
||||
|
||||
pause-stream@^0.0.11:
|
||||
version "0.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
|
||||
integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==
|
||||
dependencies:
|
||||
through "~2.3"
|
||||
|
||||
pause@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
||||
@@ -5657,16 +5607,6 @@ saslprep@^1.0.3:
|
||||
dependencies:
|
||||
sparse-bitfield "^3.0.3"
|
||||
|
||||
save@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/save/-/save-2.9.0.tgz#6659375fadeaf58e4abc6a90ec6b0fb2e2232e0f"
|
||||
integrity sha512-eg8+g8CjvehE/2C6EbLdtK1pINVD27pcJLj4M9PjWWhoeha/y5bWf4dp/0RF+OzbKTcG1bae9qi3PAqiR8CJTg==
|
||||
dependencies:
|
||||
async "^3.2.2"
|
||||
event-stream "^4.0.1"
|
||||
lodash.assign "^4.2.0"
|
||||
mingo "^6.1.0"
|
||||
|
||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
@@ -5894,13 +5834,6 @@ split-ca@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6"
|
||||
integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==
|
||||
|
||||
split@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
|
||||
integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
|
||||
dependencies:
|
||||
through "2"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
@@ -5951,14 +5884,6 @@ stop-iteration-iterator@^1.0.0:
|
||||
dependencies:
|
||||
internal-slot "^1.0.4"
|
||||
|
||||
stream-combiner@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858"
|
||||
integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==
|
||||
dependencies:
|
||||
duplexer "~0.1.1"
|
||||
through "~2.3.4"
|
||||
|
||||
string-length@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
|
||||
@@ -6211,7 +6136,7 @@ throttleit@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==
|
||||
|
||||
through@2, through@^2.3.8, through@~2.3, through@~2.3.4:
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||
|
||||
Reference in new issue
Block a user