mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
API Integration Tests via jest (#10899)
* tests: experiment with api tests * api tests are getting nice * api: api testing ready for feedback
This commit is contained in:
parent
bb681cd498
commit
ead1c300d7
@ -4,7 +4,7 @@
|
||||
"bitwise":false,
|
||||
"curly": true,
|
||||
"eqnull": true,
|
||||
"strict": true,
|
||||
"strict": false,
|
||||
"devel": true,
|
||||
"eqeqeq": true,
|
||||
"forin": false,
|
||||
|
@ -19,6 +19,7 @@
|
||||
"angular-mocks": "^1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"awesome-typescript-loader": "^3.2.3",
|
||||
"axios": "^0.17.1",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
@ -105,6 +106,7 @@
|
||||
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
||||
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
|
||||
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
|
||||
"api-tests": "node ./node_modules/jest-cli/bin/jest.js --notify --watch --config=tests/api/jest.js",
|
||||
"precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
7
tests/api/clearState.test.ts
Normal file
7
tests/api/clearState.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as setup from './setup';
|
||||
|
||||
describe.skip('clear state', () => {
|
||||
it('will clear state', () => {
|
||||
return setup.clearState();
|
||||
});
|
||||
});
|
30
tests/api/client.ts
Normal file
30
tests/api/client.ts
Normal file
@ -0,0 +1,30 @@
|
||||
const axios = require('axios');
|
||||
|
||||
export function getClient(options) {
|
||||
return axios.create({
|
||||
baseURL: 'http://localhost:3000',
|
||||
timeout: 1000,
|
||||
auth: {
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdminClient() {
|
||||
return getClient({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
});
|
||||
}
|
||||
|
||||
let client = getAdminClient();
|
||||
|
||||
client.callAs = function(user) {
|
||||
return getClient({
|
||||
username: user.login,
|
||||
password: 'password',
|
||||
});
|
||||
};
|
||||
|
||||
export default client;
|
45
tests/api/dashboard.test.ts
Normal file
45
tests/api/dashboard.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import client from './client';
|
||||
import * as setup from './setup';
|
||||
|
||||
describe('/api/dashboards', () => {
|
||||
let state: any = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
state = await setup.ensureState({
|
||||
orgName: 'api-test-org',
|
||||
users: [
|
||||
{ user: setup.admin, role: 'Admin' },
|
||||
{ user: setup.editor, role: 'Editor' },
|
||||
{ user: setup.viewer, role: 'Viewer' },
|
||||
],
|
||||
admin: setup.admin,
|
||||
dashboards: [
|
||||
{
|
||||
title: 'aaa',
|
||||
uid: 'aaa',
|
||||
},
|
||||
{
|
||||
title: 'bbb',
|
||||
uid: 'bbb',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('With admin user', () => {
|
||||
it('can delete dashboard', async () => {
|
||||
let rsp = await client.callAs(setup.admin).delete(`/api/dashboards/uid/aaa`);
|
||||
expect(rsp.data.title).toBe('aaa');
|
||||
});
|
||||
});
|
||||
|
||||
describe('With viewer user', () => {
|
||||
it('Cannot delete dashboard', async () => {
|
||||
let rsp = await setup.expectError(() => {
|
||||
return client.callAs(setup.viewer).delete(`/api/dashboards/uid/bbb`);
|
||||
});
|
||||
|
||||
expect(rsp.response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
19
tests/api/jest.js
Normal file
19
tests/api/jest.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsConfigFile": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "<rootDir>/../../node_modules/ts-jest/preprocessor.js"
|
||||
},
|
||||
"moduleDirectories": ["node_modules"],
|
||||
"testRegex": "(\\.|/)(test)\\.ts$",
|
||||
"testEnvironment": "node",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json"
|
||||
],
|
||||
};
|
27
tests/api/search.test.ts
Normal file
27
tests/api/search.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import client from './client';
|
||||
import * as setup from './setup';
|
||||
|
||||
describe('GET /api/search', () => {
|
||||
const state = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
state = await setup.ensureState({
|
||||
orgName: 'api-test-org',
|
||||
users: [{ user: setup.admin, role: 'Admin' }],
|
||||
admin: setup.admin,
|
||||
dashboards: [
|
||||
{
|
||||
title: 'Dashboard in root no permissions',
|
||||
uid: 'AAA',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('With admin user', () => {
|
||||
it('should return all dashboards', async () => {
|
||||
let rsp = await client.callAs(state.admin).get('/api/search');
|
||||
expect(rsp.data).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
107
tests/api/setup.ts
Normal file
107
tests/api/setup.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import client from './client';
|
||||
import _ from 'lodash;';
|
||||
|
||||
export const editor = {
|
||||
email: 'api-test-editor@grafana.com',
|
||||
login: 'api-test-editor',
|
||||
password: 'password',
|
||||
name: 'Api Test Editor',
|
||||
};
|
||||
|
||||
export const admin = {
|
||||
email: 'api-test-admin@grafana.com',
|
||||
login: 'api-test-admin',
|
||||
password: 'password',
|
||||
name: 'Api Test Super',
|
||||
};
|
||||
|
||||
export const viewer = {
|
||||
email: 'api-test-viewer@grafana.com',
|
||||
login: 'api-test-viewer',
|
||||
password: 'password',
|
||||
name: 'Api Test Viewer',
|
||||
};
|
||||
|
||||
export async function expectError(callback) {
|
||||
try {
|
||||
let rsp = await callback();
|
||||
return rsp;
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return rsp;
|
||||
}
|
||||
|
||||
// deletes org if it's already there
|
||||
export async function getOrg(orgName) {
|
||||
try {
|
||||
const rsp = await client.get(`/api/orgs/name/${orgName}`);
|
||||
await client.delete(`/api/orgs/${rsp.data.id}`);
|
||||
} catch {}
|
||||
|
||||
const rsp = await client.post(`/api/orgs`, { name: orgName });
|
||||
return { name: orgName, id: rsp.data.orgId };
|
||||
}
|
||||
|
||||
export async function getUser(user) {
|
||||
const search = await client.get('/api/users/search', {
|
||||
params: { query: user.login },
|
||||
});
|
||||
|
||||
if (search.data.totalCount === 1) {
|
||||
user.id = search.data.users[0].id;
|
||||
return user;
|
||||
}
|
||||
|
||||
const rsp = await client.post('/api/admin/users', user);
|
||||
user.id = rsp.data.id;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function addUserToOrg(org, user, role) {
|
||||
const rsp = await client.post(`/api/orgs/${org.id}/users`, {
|
||||
loginOrEmail: user.login,
|
||||
role: role,
|
||||
});
|
||||
|
||||
return rsp.data;
|
||||
}
|
||||
|
||||
export async function clearState() {
|
||||
const admin = await getUser(adminUser);
|
||||
const rsp = await client.delete(`/api/admin/users/${admin.id}`);
|
||||
return rsp.data;
|
||||
}
|
||||
|
||||
export async function setUsingOrg(user, org) {
|
||||
await client.callAs(user).post(`/api/user/using/${org.id}`);
|
||||
}
|
||||
|
||||
export async function createDashboard(user, dashboard) {
|
||||
const rsp = await client.callAs(user).post(`/api/dashboards/db`, {
|
||||
dashboard: dashboard,
|
||||
overwrite: true,
|
||||
});
|
||||
dashboard.id = rsp.data.id;
|
||||
dashboard.url = rsp.data.url;
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
export async function ensureState(state) {
|
||||
const org = await getOrg(state.orgName);
|
||||
|
||||
for (let orgUser of state.users) {
|
||||
const user = await getUser(orgUser.user);
|
||||
await addUserToOrg(org, user, orgUser.role);
|
||||
await setUsingOrg(user, org);
|
||||
}
|
||||
|
||||
for (let dashboard of state.dashboards) {
|
||||
await createDashboard(state.admin, dashboard);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
24
tests/api/tsconfig.json
Normal file
24
tests/api/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"lib": ["es6"],
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"inlineSourceMap": false,
|
||||
"sourceMap": true,
|
||||
"noEmitOnError": false,
|
||||
"emitDecoratorMetadata": false,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": false,
|
||||
"noImplicitUseStrict":false,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
22
tests/api/user.test.ts
Normal file
22
tests/api/user.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import client from './client';
|
||||
import * as setup from './setup';
|
||||
|
||||
describe('GET /api/user', () => {
|
||||
it('should return current authed user', async () => {
|
||||
let rsp = await client.get('/api/user');
|
||||
expect(rsp.data.login).toBe('admin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/user', () => {
|
||||
it('should update current authed user', async () => {
|
||||
const user = await setup.getUser(setup.editor);
|
||||
user.name = 'Updated via test';
|
||||
|
||||
const rsp = await client.callAs(user).put('/api/user', user);
|
||||
expect(rsp.data.message).toBe('User updated');
|
||||
|
||||
const updated = await client.callAs(user).get('/api/user');
|
||||
expect(updated.data.name).toBe('Updated via test');
|
||||
});
|
||||
});
|
91
yarn.lock
91
yarn.lock
@ -224,6 +224,14 @@
|
||||
version "16.0.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed"
|
||||
|
||||
"@types/strip-bom@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
||||
|
||||
"@types/strip-json-comments@0.0.30":
|
||||
version "0.0.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
|
||||
|
||||
JSONStream@~1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
|
||||
@ -699,6 +707,13 @@ aws4@^1.2.1, aws4@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||
|
||||
axios@^0.17.1:
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
|
||||
dependencies:
|
||||
follow-redirects "^1.2.5"
|
||||
is-buffer "^1.1.5"
|
||||
|
||||
babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
@ -1626,6 +1641,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
chalk@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.2.0"
|
||||
|
||||
chalk@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
|
||||
@ -2776,7 +2799,7 @@ diff@^2.0.2:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99"
|
||||
|
||||
diff@^3.2.0:
|
||||
diff@^3.1.0, diff@^3.2.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
|
||||
|
||||
@ -3748,6 +3771,12 @@ flush-write-stream@^1.0.0:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.4"
|
||||
|
||||
follow-redirects@^1.2.5:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
|
||||
for-in@^0.1.3:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
|
||||
@ -4385,6 +4414,10 @@ has-flag@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
||||
has-symbols@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
|
||||
@ -4513,6 +4546,12 @@ home-or-tmp@^2.0.0:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.1"
|
||||
|
||||
homedir-polyfill@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
|
||||
dependencies:
|
||||
parse-passwd "^1.0.0"
|
||||
|
||||
hooker@^0.2.3, hooker@~0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959"
|
||||
@ -6211,6 +6250,10 @@ make-dir@^1.0.0:
|
||||
dependencies:
|
||||
pify "^3.0.0"
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.3.tgz#a97ae14ffd98b05f543e83ddc395e1b2b6e4cc6a"
|
||||
|
||||
make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38"
|
||||
@ -7371,6 +7414,10 @@ parse-json@^3.0.0:
|
||||
dependencies:
|
||||
error-ex "^1.3.1"
|
||||
|
||||
parse-passwd@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
||||
|
||||
parse5@^3.0.1, parse5@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
|
||||
@ -9601,7 +9648,7 @@ strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0.
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
strip-json-comments@^2.0.0, strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
@ -9633,6 +9680,12 @@ supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0:
|
||||
dependencies:
|
||||
has-flag "^2.0.0"
|
||||
|
||||
supports-color@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
svgo@^0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
|
||||
@ -9941,6 +9994,30 @@ ts-loader@^3.2.0:
|
||||
loader-utils "^1.0.2"
|
||||
semver "^5.0.1"
|
||||
|
||||
ts-node@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-4.1.0.tgz#36d9529c7b90bb993306c408cd07f7743de20712"
|
||||
dependencies:
|
||||
arrify "^1.0.0"
|
||||
chalk "^2.3.0"
|
||||
diff "^3.1.0"
|
||||
make-error "^1.1.1"
|
||||
minimist "^1.2.0"
|
||||
mkdirp "^0.5.1"
|
||||
source-map-support "^0.5.0"
|
||||
tsconfig "^7.0.0"
|
||||
v8flags "^3.0.0"
|
||||
yn "^2.0.0"
|
||||
|
||||
tsconfig@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
|
||||
dependencies:
|
||||
"@types/strip-bom" "^3.0.0"
|
||||
"@types/strip-json-comments" "0.0.30"
|
||||
strip-bom "^3.0.0"
|
||||
strip-json-comments "^2.0.0"
|
||||
|
||||
tslib@^1.7.1:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6"
|
||||
@ -10302,6 +10379,12 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@~3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||
|
||||
v8flags@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b"
|
||||
dependencies:
|
||||
homedir-polyfill "^1.0.1"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
|
||||
@ -10766,6 +10849,10 @@ yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
|
||||
yn@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
|
||||
|
||||
zip-stream@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"
|
||||
|
Loading…
Reference in New Issue
Block a user