Compare commits
9 Commits
feat/saves
...
98a0704c55
| Author | SHA1 | Date | |
|---|---|---|---|
| 98a0704c55 | |||
| 45f33b9365 | |||
| b678f0181a | |||
| ee5cd575bf | |||
| bebea4e374 | |||
| 354e00db56 | |||
| 35fd995d2d | |||
| 74513b4de3 | |||
| 9d3fb6ae9a |
145
apps/backend/src/tests/e2e/access-control.test.ts
Normal file
145
apps/backend/src/tests/e2e/access-control.test.ts
Normal file
@ -0,0 +1,145 @@
|
||||
// apps/backend/src/tests/e2e/access-control.test.ts
|
||||
// Path: apps/backend/src/tests/e2e/access-control.test.ts
|
||||
|
||||
import { describe, expect, test, beforeAll } from 'bun:test';
|
||||
|
||||
describe('E2E: Access Control', () => {
|
||||
let user1Cookie: string;
|
||||
let user1Id: string;
|
||||
let user2Cookie: string;
|
||||
let user2Id: string;
|
||||
let linkSaveId: number;
|
||||
let linkShareUrl: string;
|
||||
let publicSaveId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Создаем первого пользователя
|
||||
const user1SignUp = await fetch('http://localhost:3000/auth/api/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'User 1',
|
||||
email: `user1-${Date.now()}@example.com`,
|
||||
password: 'Password123!',
|
||||
}),
|
||||
});
|
||||
|
||||
const user1Data = await user1SignUp.json();
|
||||
user1Id = user1Data.user.id;
|
||||
user1Cookie = user1SignUp.headers.get('set-cookie') || '';
|
||||
|
||||
// Создаем второго пользователя
|
||||
const user2SignUp = await fetch('http://localhost:3000/auth/api/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'User 2',
|
||||
email: `user2-${Date.now()}@example.com`,
|
||||
password: 'Password123!',
|
||||
}),
|
||||
});
|
||||
|
||||
const user2Data = await user2SignUp.json();
|
||||
user2Id = user2Data.user.id;
|
||||
user2Cookie = user2SignUp.headers.get('set-cookie') || '';
|
||||
|
||||
// User 1 создает сейв с visibility: link
|
||||
const linkSaveResponse = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: 'https://httpbin.org/image/png',
|
||||
name: 'Link Save',
|
||||
visibility: 'link',
|
||||
}),
|
||||
});
|
||||
|
||||
const linkSaveData = await linkSaveResponse.json();
|
||||
linkSaveId = linkSaveData.id;
|
||||
linkShareUrl = linkSaveData.shareUrl;
|
||||
|
||||
// User 1 создает публичный сейв
|
||||
const publicSaveResponse = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: 'https://httpbin.org/image/jpeg',
|
||||
name: 'Public Save',
|
||||
visibility: 'public',
|
||||
}),
|
||||
});
|
||||
|
||||
const publicSaveData = await publicSaveResponse.json();
|
||||
publicSaveId = publicSaveData.id;
|
||||
});
|
||||
|
||||
test('owner should access link save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${linkSaveId}`, {
|
||||
headers: { 'Cookie': user1Cookie },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test('non-owner should NOT access link save without share token', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${linkSaveId}`, {
|
||||
headers: { 'Cookie': user2Cookie },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test('non-owner should access link save WITH share token', async () => {
|
||||
const response = await fetch(
|
||||
`http://localhost:3000/saves/${linkSaveId}?share=${linkShareUrl}`,
|
||||
{
|
||||
headers: { 'Cookie': user2Cookie },
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.id).toBe(linkSaveId);
|
||||
});
|
||||
|
||||
test('anyone should access public save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${publicSaveId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.id).toBe(publicSaveId);
|
||||
expect(data.visibility).toBe('public');
|
||||
});
|
||||
|
||||
test('non-owner should NOT be able to update save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${publicSaveId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': user2Cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Hacked Name',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('non-owner should NOT be able to delete save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${publicSaveId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Cookie': user2Cookie },
|
||||
});
|
||||
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
});
|
||||
95
apps/backend/src/tests/e2e/auth.test.ts
Normal file
95
apps/backend/src/tests/e2e/auth.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { describe, expect, test, } from 'bun:test';
|
||||
|
||||
describe('E2E: Authentication', () => {
|
||||
const testUser = {
|
||||
name: 'Test User',
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: 'TestPassword123!',
|
||||
};
|
||||
|
||||
let authCookie: string;
|
||||
let userId: string;
|
||||
|
||||
test('should register new user', async () => {
|
||||
const response = await fetch('http://localhost:3000/auth/api/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(testUser),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json() as unknown as any;
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user.email).toBe(testUser.email);
|
||||
expect(data.user.name).toBe(testUser.name);
|
||||
|
||||
userId = data.user.id;
|
||||
|
||||
// Получаем cookies из ответа
|
||||
const setCookieHeader = response.headers.get('set-cookie');
|
||||
expect(setCookieHeader).toBeDefined();
|
||||
authCookie = setCookieHeader!;
|
||||
});
|
||||
|
||||
test('should get current session with cookies', async () => {
|
||||
const response = await fetch('http://localhost:3000/auth/api/get-session', {
|
||||
headers: {
|
||||
'Cookie': authCookie,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json() as any;
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user.email).toBe(testUser.email);
|
||||
});
|
||||
|
||||
test('should sign in with credentials', async () => {
|
||||
const response = await fetch('http://localhost:3000/auth/api/sign-in/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json() as any;
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user.email).toBe(testUser.email);
|
||||
|
||||
const setCookieHeader = response.headers.get('set-cookie');
|
||||
expect(setCookieHeader).toBeDefined();
|
||||
authCookie = setCookieHeader!;
|
||||
});
|
||||
|
||||
test('should fail with wrong password', async () => {
|
||||
const response = await fetch('http://localhost:3000/auth/api/sign-in/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: testUser.email,
|
||||
password: 'WrongPassword',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should fail without cookies', async () => {
|
||||
const response = await fetch('http://localhost:3000/auth/api/session');
|
||||
|
||||
// Должен вернуть 401 или отсутствующую сессию
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
});
|
||||
146
apps/backend/src/tests/e2e/saves.test.ts
Normal file
146
apps/backend/src/tests/e2e/saves.test.ts
Normal file
@ -0,0 +1,146 @@
|
||||
// apps/backend/src/tests/e2e/saves.test.ts
|
||||
// Path: apps/backend/src/tests/e2e/saves.test.ts
|
||||
|
||||
import { describe, expect, test, beforeAll } from 'bun:test';
|
||||
|
||||
describe('E2E: Saves Management', () => {
|
||||
let user1Cookie: string;
|
||||
let user1Id: string;
|
||||
let saveId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Создаем тестового пользователя
|
||||
const signUpResponse = await fetch('http://localhost:3000/auth/api/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Save Test User',
|
||||
email: `save-test-${Date.now()}@example.com`,
|
||||
password: 'TestPassword123!',
|
||||
}),
|
||||
});
|
||||
|
||||
const signUpData = await signUpResponse.json();
|
||||
user1Id = signUpData.user.id;
|
||||
|
||||
// Получаем cookies
|
||||
const setCookieHeader = signUpResponse.headers.get('set-cookie');
|
||||
if (setCookieHeader) {
|
||||
user1Cookie = setCookieHeader;
|
||||
}
|
||||
});
|
||||
|
||||
test('should create save from external URL', async () => {
|
||||
const response = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: 'https://www.pinterest.com/pin/40391727901690267/',
|
||||
name: 'Test Image',
|
||||
description: 'A test image from httpbin',
|
||||
tags: ['test', 'e2e'],
|
||||
visibility: 'link',
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.name).toBe('Test Image');
|
||||
expect(data.type).toBe('image');
|
||||
expect(data.visibility).toBe('link');
|
||||
expect(data.shareUrl).toBeDefined();
|
||||
|
||||
saveId = data.id;
|
||||
});
|
||||
|
||||
test('should get my saves', async () => {
|
||||
const response = await fetch('http://localhost:3000/saves/my', {
|
||||
headers: {
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(Array.isArray(data)).toBe(true);
|
||||
expect(data.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should get save by ID', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${saveId}`, {
|
||||
headers: {
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.id).toBe(saveId);
|
||||
expect(data.name).toBe('Test Image');
|
||||
});
|
||||
|
||||
test('should update save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${saveId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Updated Test Image',
|
||||
description: 'Updated description',
|
||||
tags: ['updated', 'test'],
|
||||
visibility: 'public',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.name).toBe('Updated Test Image');
|
||||
expect(data.visibility).toBe('public');
|
||||
expect(data.tags).toContain('updated');
|
||||
});
|
||||
|
||||
test('should access public save without auth', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${saveId}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.id).toBe(saveId);
|
||||
});
|
||||
|
||||
test('should delete save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${saveId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(true);
|
||||
});
|
||||
|
||||
test('should return 404 for deleted save', async () => {
|
||||
const response = await fetch(`http://localhost:3000/saves/${saveId}`, {
|
||||
headers: {
|
||||
'Cookie': user1Cookie,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
88
apps/backend/src/tests/e2e/scraper.test.ts
Normal file
88
apps/backend/src/tests/e2e/scraper.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
// apps/backend/src/tests/e2e/scraper.test.ts
|
||||
// Path: apps/backend/src/tests/e2e/scraper.test.ts
|
||||
|
||||
import { describe, expect, test, beforeAll } from 'bun:test';
|
||||
|
||||
describe('E2E: Scraper Integration', () => {
|
||||
let authCookie: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const signUpResponse = await fetch('http://localhost:3000/auth/api/sign-up/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'Scraper Test User',
|
||||
email: `scraper-test-${Date.now()}@example.com`,
|
||||
password: 'TestPassword123!',
|
||||
}),
|
||||
});
|
||||
|
||||
authCookie = signUpResponse.headers.get('set-cookie') || '';
|
||||
});
|
||||
|
||||
test('should save from direct image URL', async () => {
|
||||
const response = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': authCookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: 'https://mrqiz.ru/',
|
||||
name: 'Direct Image Test',
|
||||
visibility: 'public',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.type).toBe('image');
|
||||
expect(data.name).toBe('Direct Image Test');
|
||||
});
|
||||
|
||||
// Эти тесты требуют реальных URL
|
||||
test.skip('should save from Pinterest URL (manual test)', async () => {
|
||||
// Замените на актуальный Pinterest URL
|
||||
const pinterestUrl = 'https://www.pinterest.com/pin/ACTUAL_PIN_ID/';
|
||||
|
||||
const response = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': authCookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: pinterestUrl,
|
||||
visibility: 'link',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(['image', 'video', 'gif']).toContain(data.type);
|
||||
});
|
||||
|
||||
test.skip('should save from Tenor GIF (manual test)', async () => {
|
||||
// Замените на актуальный Tenor URL
|
||||
const tenorUrl = 'https://tenor.com/view/ACTUAL_GIF_ID';
|
||||
|
||||
const response = await fetch('http://localhost:3000/saves/external', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': authCookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: tenorUrl,
|
||||
visibility: 'link',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.type).toBe('gif');
|
||||
});
|
||||
});
|
||||
19
apps/backend/src/tests/setup.ts
Normal file
19
apps/backend/src/tests/setup.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { beforeAll, afterAll } from 'bun:test';
|
||||
import { redis } from '@/services/redis.service';
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log('Setting up test environment...');
|
||||
|
||||
try {
|
||||
await redis.connect();
|
||||
} catch (error) {
|
||||
console.warn('Redis not available in tests, continuing without cache');
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
console.log('Cleaning up test environment...');
|
||||
|
||||
await redis.disconnect();
|
||||
});
|
||||
|
||||
257
apps/backend/src/tests/unit/saves.service.test.ts
Normal file
257
apps/backend/src/tests/unit/saves.service.test.ts
Normal file
@ -0,0 +1,257 @@
|
||||
import { describe, expect, test, mock, beforeEach } from 'bun:test';
|
||||
import { s3Service } from '@/services/s3.service';
|
||||
|
||||
// Моки
|
||||
const mockDb = {
|
||||
insert: mock(() => ({
|
||||
values: mock(() => ({
|
||||
returning: mock(() => [
|
||||
{
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
name: 'Test Save',
|
||||
description: 'Test description',
|
||||
type: 'image',
|
||||
tags: ['test'],
|
||||
visibility: 'link',
|
||||
shareUrl: 'abc123',
|
||||
s3Key: 'user1/test.jpg',
|
||||
url: 'http://s3/bucket/user1/test.jpg',
|
||||
fileSize: 1024,
|
||||
mimeType: 'image/jpeg',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]),
|
||||
})),
|
||||
})),
|
||||
select: mock(() => ({
|
||||
from: mock(() => ({
|
||||
where: mock(() => ({
|
||||
limit: mock(() => [
|
||||
{
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
name: 'Test Save',
|
||||
description: 'Test description',
|
||||
type: 'image',
|
||||
tags: ['test'],
|
||||
visibility: 'link',
|
||||
shareUrl: 'abc123',
|
||||
s3Key: 'user1/test.jpg',
|
||||
url: 'http://s3/bucket/user1/test.jpg',
|
||||
fileSize: 1024,
|
||||
mimeType: 'image/jpeg',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]),
|
||||
orderBy: mock(() => []),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
update: mock(() => ({
|
||||
set: mock(() => ({
|
||||
where: mock(() => ({
|
||||
returning: mock(() => []),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
delete: mock(() => ({
|
||||
where: mock(() => ({})),
|
||||
})),
|
||||
};
|
||||
|
||||
const mockS3Service = {
|
||||
uploadFile: mock(async () => ({
|
||||
key: 'user1/test.jpg',
|
||||
url: 'http://s3/bucket/user1/test.jpg',
|
||||
size: 1024,
|
||||
})),
|
||||
deleteFile: mock(async () => {}),
|
||||
downloadFromUrl: mock(async () => ({
|
||||
buffer: Buffer.from('test'),
|
||||
mimeType: 'image/jpeg',
|
||||
size: 4,
|
||||
})),
|
||||
getFileType: mock((mimeType: string) => 'image'),
|
||||
};
|
||||
|
||||
describe('SavesService', () => {
|
||||
beforeEach(() => {
|
||||
// Сбрасываем моки перед каждым тестом
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('createFromFile', () => {
|
||||
test('should create save from uploaded file', async () => {
|
||||
const file = new File(['test content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const userId = 'user1';
|
||||
const metadata = {
|
||||
name: 'Test Image',
|
||||
description: 'A test image',
|
||||
tags: ['test', 'image'],
|
||||
visibility: 'link' as const,
|
||||
};
|
||||
|
||||
// Здесь в реальном тесте нужно использовать моки или тестовую БД
|
||||
// Для примера проверяем структуру
|
||||
expect(file.type).toBe('image/jpeg');
|
||||
expect(file.name).toBe('test.jpg');
|
||||
});
|
||||
|
||||
test('should throw error for unsupported file type', async () => {
|
||||
const file = new File(['test'], 'test.txt', { type: 'text/plain' });
|
||||
const userId = 'user1';
|
||||
|
||||
// В реальном тесте проверяем, что выбрасывается ошибка
|
||||
expect(() => {
|
||||
if (s3Service.getFileType(file.type) === 'unknown') {
|
||||
throw new Error('Unsupported file type');
|
||||
}
|
||||
}).toThrow('Unsupported file type');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFromUrl', () => {
|
||||
test('should create save from external URL', async () => {
|
||||
const userId = 'user1';
|
||||
const data = {
|
||||
url: 'https://example.com/image.jpg',
|
||||
name: 'External Image',
|
||||
description: 'Downloaded image',
|
||||
tags: ['external'],
|
||||
visibility: 'public' as const,
|
||||
};
|
||||
|
||||
// Проверяем валидность данных
|
||||
expect(data.url).toMatch(/^https?:\/\//);
|
||||
expect(data.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
test('should return save for owner', async () => {
|
||||
const saveId = 1;
|
||||
const userId = 'user1';
|
||||
|
||||
// Мокаем сейв
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
visibility: 'link',
|
||||
shareUrl: 'abc123',
|
||||
};
|
||||
|
||||
// Владелец имеет доступ
|
||||
expect(mockSave.userId).toBe(userId);
|
||||
});
|
||||
|
||||
test('should return save for public visibility', async () => {
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
visibility: 'public',
|
||||
};
|
||||
|
||||
// Публичный сейв доступен всем
|
||||
expect(mockSave.visibility).toBe('public');
|
||||
});
|
||||
|
||||
test('should return save with valid share token', async () => {
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
visibility: 'link',
|
||||
shareUrl: 'abc123',
|
||||
};
|
||||
const shareToken = 'abc123';
|
||||
|
||||
// Доступ по share token
|
||||
expect(mockSave.shareUrl).toBe(shareToken);
|
||||
});
|
||||
|
||||
test('should not return save without access', async () => {
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
visibility: 'link',
|
||||
shareUrl: 'abc123',
|
||||
};
|
||||
const requestUserId = 'user2';
|
||||
const shareToken = 'wrong-token';
|
||||
|
||||
// Нет доступа
|
||||
const hasAccess =
|
||||
mockSave.userId === requestUserId ||
|
||||
mockSave.visibility === 'public' ||
|
||||
(mockSave.visibility === 'link' && mockSave.shareUrl === shareToken);
|
||||
|
||||
expect(hasAccess).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
test('should update save metadata', async () => {
|
||||
const saveId = 1;
|
||||
const userId = 'user1';
|
||||
const updateData = {
|
||||
name: 'Updated Name',
|
||||
description: 'Updated description',
|
||||
tags: ['updated'],
|
||||
};
|
||||
|
||||
// Проверяем данные для обновления
|
||||
expect(updateData.name).toBe('Updated Name');
|
||||
expect(updateData.tags).toContain('updated');
|
||||
});
|
||||
|
||||
test('should generate shareUrl when changing to link visibility', async () => {
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
visibility: 'public',
|
||||
shareUrl: null,
|
||||
};
|
||||
|
||||
const newVisibility = 'link';
|
||||
|
||||
// Должен быть сгенерирован shareUrl
|
||||
if (newVisibility === 'link' && !mockSave.shareUrl) {
|
||||
const generatedUrl = 'generated-url';
|
||||
expect(generatedUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
test('should delete save and file from S3', async () => {
|
||||
const saveId = 1;
|
||||
const userId = 'user1';
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
s3Key: 'user1/test.jpg',
|
||||
};
|
||||
|
||||
// Проверяем, что есть s3Key для удаления
|
||||
expect(mockSave.s3Key).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should throw error when deleting non-owned save', async () => {
|
||||
const mockSave = {
|
||||
id: 1,
|
||||
userId: 'user1',
|
||||
};
|
||||
const requestUserId = 'user2';
|
||||
|
||||
// Проверка владельца
|
||||
expect(() => {
|
||||
if (mockSave.userId !== requestUserId) {
|
||||
throw new Error('Save not found or access denied');
|
||||
}
|
||||
}).toThrow('access denied');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
71
apps/backend/src/tests/unit/scraper.service.test.ts
Normal file
71
apps/backend/src/tests/unit/scraper.service.test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { scraperService } from '@/services/scraper.service';
|
||||
|
||||
describe('ScraperService', () => {
|
||||
describe('scrapeUrl', () => {
|
||||
test('should detect direct image URL', async () => {
|
||||
const url = 'https://example.com/image.jpg';
|
||||
const result = await scraperService.scrapeUrl(url);
|
||||
|
||||
expect(result.source).toBe('direct');
|
||||
expect(result.type).toBe('image');
|
||||
expect(result.url).toBe(url);
|
||||
});
|
||||
|
||||
test('should detect direct GIF URL', async () => {
|
||||
const url = 'https://example.com/animation.gif';
|
||||
const result = await scraperService.scrapeUrl(url);
|
||||
|
||||
expect(result.source).toBe('direct');
|
||||
expect(result.type).toBe('gif');
|
||||
expect(result.url).toBe(url);
|
||||
});
|
||||
|
||||
test('should detect direct video URL', async () => {
|
||||
const url = 'https://example.com/video.mp4';
|
||||
const result = await scraperService.scrapeUrl(url);
|
||||
|
||||
expect(result.source).toBe('direct');
|
||||
expect(result.type).toBe('video');
|
||||
expect(result.url).toBe(url);
|
||||
});
|
||||
|
||||
test('should reject unsupported URL', async () => {
|
||||
const url = 'https://unsupported-site.com/page';
|
||||
|
||||
expect(scraperService.scrapeUrl(url)).rejects.toThrow(
|
||||
'Unsupported URL'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrapePinterest', () => {
|
||||
test('should recognize Pinterest URL', async () => {
|
||||
const url = 'https://www.pinterest.com/pin/123456789/';
|
||||
|
||||
try {
|
||||
const result = await scraperService.scrapeUrl(url);
|
||||
expect(result.source).toBe('pinterest');
|
||||
expect(['image', 'video']).toContain(result.type);
|
||||
} catch (error) {
|
||||
// В тестовой среде без интернета это нормально
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrapeTenor', () => {
|
||||
test('should recognize Tenor URL', async () => {
|
||||
const url = 'https://tenor.com/view/test-gif-12345678';
|
||||
|
||||
try {
|
||||
const result = await scraperService.scrapeUrl(url);
|
||||
expect(result.source).toBe('tenor');
|
||||
expect(result.type).toBe('gif');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user