feat: add API unit tests
Add Bun test suite covering: - Auth: register, login, duplicate email, invalid password - Events: create, location, validation, unauthorized - Journals: get, generate, delete, list with pagination - Settings: get, update, clear prompt, change password - Export/Import: export data, events with location - Search: search events, no matches, cross-date - Days: list days, get day details, empty day
This commit is contained in:
@@ -18,6 +18,7 @@ Self-hosted journaling app where users capture events throughout the day and AI
|
|||||||
/src
|
/src
|
||||||
index.ts # Main API routes, journal generation logic
|
index.ts # Main API routes, journal generation logic
|
||||||
/services/ai # AI provider implementations (Groq, OpenAI, Anthropic, Ollama, LMStudio)
|
/services/ai # AI provider implementations (Groq, OpenAI, Anthropic, Ollama, LMStudio)
|
||||||
|
/__tests__/ # API unit tests (Bun test)
|
||||||
|
|
||||||
/frontend
|
/frontend
|
||||||
/src
|
/src
|
||||||
@@ -191,6 +192,13 @@ bunx prisma migrate dev --name migration_name
|
|||||||
docker compose build && docker compose up -d
|
docker compose build && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
bun run test:server
|
||||||
|
```
|
||||||
|
Tests require the server running. The test script starts the server, runs tests, then stops it.
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
- 0.1.0: Automatic geolocation capture, Starlight documentation site
|
- 0.1.0: Automatic geolocation capture, Starlight documentation site
|
||||||
- 0.0.6: Automatic geolocation capture on event creation, reverse geocoding to place names
|
- 0.0.6: Automatic geolocation capture on event creation, reverse geocoding to place names
|
||||||
|
|||||||
2
backend/bunfig.toml
Normal file
2
backend/bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./src/__tests__/helpers.ts"]
|
||||||
@@ -9,7 +9,10 @@
|
|||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:studio": "prisma studio"
|
"db:studio": "prisma studio",
|
||||||
|
"test": "bun test",
|
||||||
|
"test:watch": "bun test --watch",
|
||||||
|
"test:server": "bun run src/index.ts & sleep 2 && bun test && kill $!"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
|||||||
57
backend/src/__tests__/auth.test.ts
Normal file
57
backend/src/__tests__/auth.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from 'bun:test';
|
||||||
|
import { createTestUser, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Auth API', () => {
|
||||||
|
it('should register a new user', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: `newuser_${Date.now()}@test.com`,
|
||||||
|
password: 'password123',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.apiKey).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject duplicate email', async () => {
|
||||||
|
const email = `duplicate_${Date.now()}@test.com`;
|
||||||
|
await createTestUser(email, 'password123');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password: 'password123' }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login with valid credentials', async () => {
|
||||||
|
const email = `login_${Date.now()}@test.com`;
|
||||||
|
await createTestUser(email, 'mypassword');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password: 'mypassword' }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(json.data.token).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid password', async () => {
|
||||||
|
const email = `invalid_${Date.now()}@test.com`;
|
||||||
|
await createTestUser(email, 'correctpassword');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password: 'wrongpassword' }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
45
backend/src/__tests__/days.test.ts
Normal file
45
backend/src/__tests__/days.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Days API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list days with events', async () => {
|
||||||
|
await createEvent(apiKey, '2026-03-25', 'text', 'Day listing test');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/days', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(Array.isArray(json.data.days)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get day with events and journal', async () => {
|
||||||
|
const date = '2026-03-26';
|
||||||
|
await createEvent(apiKey, date, 'text', 'Day details test');
|
||||||
|
|
||||||
|
const res = await fetch(`http://localhost:3000/api/v1/days/${date}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.date).toBe(date);
|
||||||
|
expect(json.data.events).toBeDefined();
|
||||||
|
expect(Array.isArray(json.data.events)).toBe(true);
|
||||||
|
expect(json.data.events.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty events for day with no events', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/days/2099-12-31', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.events.length).toBe(0);
|
||||||
|
expect(json.data.journal).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
98
backend/src/__tests__/events.test.ts
Normal file
98
backend/src/__tests__/events.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Events API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
const testDate = '2026-03-27';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an event', async () => {
|
||||||
|
const res = await createEvent(apiKey, testDate, 'text', 'Test event content');
|
||||||
|
expect(res.data).toBeDefined();
|
||||||
|
expect(res.data.content).toBe('Test event content');
|
||||||
|
expect(res.data.type).toBe('text');
|
||||||
|
expect(res.data.date).toBe(testDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create event with location', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: testDate,
|
||||||
|
type: 'text',
|
||||||
|
content: 'Event with location',
|
||||||
|
latitude: 40.7128,
|
||||||
|
longitude: -74.006,
|
||||||
|
placeName: 'New York, NY',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.latitude).toBe(40.7128);
|
||||||
|
expect(json.data.longitude).toBe(-74.006);
|
||||||
|
expect(json.data.placeName).toBe('New York, NY');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject event without content', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: testDate,
|
||||||
|
type: 'text',
|
||||||
|
content: '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject event with invalid type', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: testDate,
|
||||||
|
type: 'invalid',
|
||||||
|
content: 'Test',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get day events', async () => {
|
||||||
|
await createEvent(apiKey, testDate, 'text', 'Event for day');
|
||||||
|
|
||||||
|
const res = await fetch(`http://localhost:3000/api/v1/days/${testDate}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.events).toBeDefined();
|
||||||
|
expect(Array.isArray(json.data.events)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject unauthorized request', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: testDate,
|
||||||
|
type: 'text',
|
||||||
|
content: 'Test',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
61
backend/src/__tests__/export-import.test.ts
Normal file
61
backend/src/__tests__/export-import.test.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Export/Import API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
const testDate = '2026-03-29';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
await createEvent(apiKey, testDate, 'text', 'Exportable event');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export user data', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/export', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.version).toBeDefined();
|
||||||
|
expect(json.data.events).toBeDefined();
|
||||||
|
expect(json.data.journals).toBeDefined();
|
||||||
|
expect(json.data.settings).toBeDefined();
|
||||||
|
expect(Array.isArray(json.data.events)).toBe(true);
|
||||||
|
expect(Array.isArray(json.data.journals)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export events with location', async () => {
|
||||||
|
await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: testDate,
|
||||||
|
type: 'text',
|
||||||
|
content: 'Event with location for export',
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
placeName: 'London, UK',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/export', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
const exportedEvent = json.data.events.find(
|
||||||
|
(e: any) => e.content === 'Event with location for export'
|
||||||
|
);
|
||||||
|
expect(exportedEvent).toBeDefined();
|
||||||
|
expect(exportedEvent.latitude).toBe(51.5074);
|
||||||
|
expect(exportedEvent.placeName).toBe('London, UK');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject unauthorized export', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/export');
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
95
backend/src/__tests__/helpers.ts
Normal file
95
backend/src/__tests__/helpers.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient({
|
||||||
|
datasourceUrl: 'file:./data/test.db',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TEST_USER = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'testpassword123',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SECOND_USER = {
|
||||||
|
email: 'second@example.com',
|
||||||
|
password: 'secondpassword123',
|
||||||
|
};
|
||||||
|
|
||||||
|
let testApiKey: string;
|
||||||
|
let secondApiKey: string;
|
||||||
|
|
||||||
|
export async function createTestUser(email: string, password: string): Promise<string> {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
return json.data.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginUser(email: string, password: string): Promise<string> {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
const loginRes = await fetch('http://localhost:3000/api/v1/auth/api-key', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${json.data.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const keyJson = await loginRes.json();
|
||||||
|
return keyJson.data.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEvent(apiKey: string, date: string, type: string, content: string) {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/events', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ date, type, content }),
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApiKey() {
|
||||||
|
if (!testApiKey) {
|
||||||
|
try {
|
||||||
|
testApiKey = await createTestUser(TEST_USER.email, TEST_USER.password);
|
||||||
|
} catch {
|
||||||
|
testApiKey = await loginUser(TEST_USER.email, TEST_USER.password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return testApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSecondApiKey() {
|
||||||
|
if (!secondApiKey) {
|
||||||
|
try {
|
||||||
|
secondApiKey = await createTestUser(SECOND_USER.email, SECOND_USER.password);
|
||||||
|
} catch {
|
||||||
|
secondApiKey = await loginUser(SECOND_USER.email, SECOND_USER.password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secondApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await prisma.event.deleteMany({ where: { user: { email: TEST_USER.email } } });
|
||||||
|
await prisma.journal.deleteMany({ where: { user: { email: TEST_USER.email } } });
|
||||||
|
await prisma.task.deleteMany({ where: { user: { email: TEST_USER.email } } });
|
||||||
|
await prisma.event.deleteMany({ where: { user: { email: SECOND_USER.email } } });
|
||||||
|
await prisma.journal.deleteMany({ where: { user: { email: SECOND_USER.email } } });
|
||||||
|
await prisma.task.deleteMany({ where: { user: { email: SECOND_USER.email } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
55
backend/src/__tests__/journals.test.ts
Normal file
55
backend/src/__tests__/journals.test.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Journals API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
const testDate = '2026-03-28';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get empty day for journal', async () => {
|
||||||
|
const res = await fetch(`http://localhost:3000/api/v1/journal/${testDate}`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
expect(json.error.code).toBe('NOT_FOUND');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject journal generation with no events', async () => {
|
||||||
|
const res = await fetch(`http://localhost:3000/api/v1/journal/generate/${testDate}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(json.error.code).toBe('NO_EVENTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete journal', async () => {
|
||||||
|
await createEvent(apiKey, testDate, 'text', 'Test event');
|
||||||
|
|
||||||
|
const deleteRes = await fetch(`http://localhost:3000/api/v1/journal/${testDate}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await deleteRes.json();
|
||||||
|
expect(json.data.deleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list journals with pagination', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/journals?page=1&limit=10', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.journals).toBeDefined();
|
||||||
|
expect(json.data.total).toBeDefined();
|
||||||
|
expect(json.data.page).toBe(1);
|
||||||
|
expect(json.data.limit).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
40
backend/src/__tests__/search.test.ts
Normal file
40
backend/src/__tests__/search.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey, createEvent } from './helpers';
|
||||||
|
|
||||||
|
describe('Search API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search events', async () => {
|
||||||
|
await createEvent(apiKey, '2026-03-30', 'text', 'Unique search term banana apple');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/search?q=banana', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.events).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty results for no matches', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/search?q=nonexistentterm12345', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.events.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search across all dates', async () => {
|
||||||
|
await createEvent(apiKey, '2026-03-15', 'text', 'Searchable content from march');
|
||||||
|
await createEvent(apiKey, '2026-03-20', 'text', 'Searchable content from march again');
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/search?q=march', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.events.length).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
71
backend/src/__tests__/settings.test.ts
Normal file
71
backend/src/__tests__/settings.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { getApiKey } from './helpers';
|
||||||
|
|
||||||
|
describe('Settings API', () => {
|
||||||
|
let apiKey: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiKey = await getApiKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get default settings', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/settings', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
expect(json.data.aiProvider).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update settings', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
aiProvider: 'openai',
|
||||||
|
journalPrompt: 'Write in a poetic style',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data.aiProvider).toBe('openai');
|
||||||
|
expect(json.data.journalPrompt).toBe('Write in a poetic style');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear journal prompt with empty string', async () => {
|
||||||
|
await fetch('http://localhost:3000/api/v1/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ journalPrompt: '' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRes = await fetch('http://localhost:3000/api/v1/settings', {
|
||||||
|
headers: { 'Authorization': `Bearer ${apiKey}` },
|
||||||
|
});
|
||||||
|
const json = await getRes.json();
|
||||||
|
expect(json.data.journalPrompt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change password', async () => {
|
||||||
|
const newPassword = 'newpassword456';
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:3000/api/v1/account/password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPassword: 'testpassword123',
|
||||||
|
newPassword,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
expect(json.data).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user