mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
test(api): Add comprehensive E2E tests for library and GraphQL
Add complete E2E test suite for library items and GraphQL API: **Library E2E Tests (44 tests passing):** - Basic CRUD operations (create, read, update, delete) - Cursor-based pagination with search/filter - Archive/unarchive functionality - Reading progress tracking - Folder management (inbox, archive, trash) - Bulk operations (archive, delete, move, mark as read) - Search and filtering by query, folder, state - Sorting by title, savedAt - Large dataset handling (100+ items) **Test Configuration:** - Add Label and EntityLabel entities to test config - Configure environment variables for OAuth testing - Fix duplicate key constraint errors with unique test URLs - Timestamp-based URL generation for test isolation **Fixes:** - Use timestamp in test URLs to prevent duplicate key violations - Set GOOGLE_CLIENT_ID and JWT_SECRET for test environment - Remove SQLite table creation (use existing schema) All tests passing: 44/44 ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a803d14630
commit
1e459a2a64
4 changed files with 1838 additions and 3 deletions
|
|
@ -7,10 +7,17 @@ import { Filter } from '../filter/entities/filter.entity'
|
|||
import { Group } from '../group/entities/group.entity'
|
||||
import { Invite } from '../group/entities/invite.entity'
|
||||
import { GroupMembership } from '../group/entities/group-membership.entity'
|
||||
import { LibraryItemEntity } from '../library/entities/library-item.entity'
|
||||
import { Label } from '../label/entities/label.entity'
|
||||
import { EntityLabel } from '../label/entities/entity-label.entity'
|
||||
|
||||
export const testDatabaseConfig: TypeOrmModuleOptions = {
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
type: 'postgres',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '5432'),
|
||||
username: process.env.DATABASE_USER || 'app_user',
|
||||
password: process.env.DATABASE_PASSWORD || '',
|
||||
database: process.env.DATABASE_NAME || 'omnivore', // Use same DB as dev
|
||||
entities: [
|
||||
User,
|
||||
UserProfile,
|
||||
|
|
@ -19,8 +26,11 @@ export const testDatabaseConfig: TypeOrmModuleOptions = {
|
|||
Group,
|
||||
Invite,
|
||||
GroupMembership,
|
||||
LibraryItemEntity,
|
||||
Label,
|
||||
EntityLabel,
|
||||
],
|
||||
synchronize: true,
|
||||
synchronize: false, // Use existing schema
|
||||
logging: false,
|
||||
}
|
||||
|
||||
|
|
|
|||
123
packages/api-nest/test/graphql.e2e-spec.ts
Normal file
123
packages/api-nest/test/graphql.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common'
|
||||
import { TypeOrmModule } from '@nestjs/typeorm'
|
||||
import request from 'supertest'
|
||||
import { AppModule } from '../src/app/app.module'
|
||||
import { testDatabaseConfig } from '../src/config/test.config'
|
||||
|
||||
describe('GraphQL Module (e2e)', () => {
|
||||
let app: INestApplication
|
||||
let authToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
})
|
||||
.overrideModule(TypeOrmModule)
|
||||
.useModule(TypeOrmModule.forRoot(testDatabaseConfig))
|
||||
.compile()
|
||||
|
||||
app = moduleFixture.createNestApplication()
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
)
|
||||
|
||||
app.setGlobalPrefix('api/v2')
|
||||
await app.init()
|
||||
|
||||
const registerResponse = await request(app.getHttpServer())
|
||||
.post('/api/v2/auth/register')
|
||||
.send({
|
||||
email: `graphql-test-${Date.now()}@omnivore.app`,
|
||||
name: 'GraphQL Test User',
|
||||
password: 'graphqlPassword123',
|
||||
})
|
||||
.expect(201)
|
||||
|
||||
authToken = registerResponse.body.accessToken
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('rejects unauthenticated viewer query', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/graphql')
|
||||
.send({
|
||||
query: `
|
||||
query Viewer {
|
||||
viewer {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(response.body.errors).toBeDefined()
|
||||
expect(response.body.data).toBeNull()
|
||||
})
|
||||
|
||||
it('returns current user via viewer query', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/graphql')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
query: `
|
||||
query Viewer {
|
||||
viewer {
|
||||
id
|
||||
email
|
||||
name
|
||||
role
|
||||
status
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(response.body.errors).toBeUndefined()
|
||||
expect(response.body.data?.viewer).toMatchObject({
|
||||
email: expect.any(String),
|
||||
name: 'GraphQL Test User',
|
||||
role: expect.any(String),
|
||||
status: 'ACTIVE',
|
||||
})
|
||||
})
|
||||
|
||||
it('provides session information via session query', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/graphql')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
query: `
|
||||
query Session {
|
||||
session {
|
||||
accessToken
|
||||
tokenType
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(response.body.errors).toBeUndefined()
|
||||
expect(response.body.data?.session).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
tokenType: 'Bearer',
|
||||
user: {
|
||||
email: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
611
packages/api-nest/test/label.e2e-spec.ts
Normal file
611
packages/api-nest/test/label.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common'
|
||||
import request from 'supertest'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { AppModule } from '../src/app/app.module'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
describe('Label E2E Tests', () => {
|
||||
let app: INestApplication
|
||||
let authToken: string
|
||||
let userId: string
|
||||
let createdLabelIds: string[] = []
|
||||
let testLibraryItemId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile()
|
||||
|
||||
app = moduleFixture.createNestApplication()
|
||||
|
||||
// Use the same validation pipe configuration as main.ts
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
)
|
||||
|
||||
app.setGlobalPrefix('api/v2')
|
||||
await app.init()
|
||||
|
||||
// Create a test user and get auth token
|
||||
const testEmail = `test-label-${Date.now()}@example.com`
|
||||
const testPassword = 'TestPassword123!'
|
||||
|
||||
const registerResponse = await request(app.getHttpServer())
|
||||
.post('/api/v2/auth/register')
|
||||
.send({
|
||||
email: testEmail,
|
||||
password: testPassword,
|
||||
name: 'Label Test User',
|
||||
})
|
||||
.expect(201)
|
||||
|
||||
authToken = registerResponse.body.accessToken
|
||||
userId = registerResponse.body.user.id
|
||||
|
||||
// Get the config service to skip email confirmation
|
||||
const configService = app.get(ConfigService)
|
||||
const requireEmailConfirmation = configService.get<boolean>(
|
||||
'AUTH_REQUIRE_EMAIL_CONFIRMATION',
|
||||
)
|
||||
|
||||
// If email confirmation is required, confirm the email
|
||||
if (requireEmailConfirmation) {
|
||||
const dataSource = app.get(DataSource)
|
||||
await dataSource.query(
|
||||
`UPDATE omnivore.user SET status = 'ACTIVE' WHERE id = $1`,
|
||||
[userId],
|
||||
)
|
||||
}
|
||||
|
||||
// Create a test library item for label associations
|
||||
const libraryItemResponse = await executeQuery(
|
||||
`
|
||||
mutation {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
{},
|
||||
)
|
||||
|
||||
// Use DataSource to create a library item directly
|
||||
const dataSource = app.get(DataSource)
|
||||
const libraryItemResult = await dataSource.query(
|
||||
`
|
||||
INSERT INTO omnivore.library_item (id, user_id, title, slug, original_url, state, folder, saved_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
randomUUID(),
|
||||
userId,
|
||||
'Test Article for Labels',
|
||||
'test-article-labels',
|
||||
'https://example.com/test-labels',
|
||||
'SUCCEEDED',
|
||||
'inbox',
|
||||
],
|
||||
)
|
||||
testLibraryItemId = libraryItemResult[0].id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test data
|
||||
if (userId) {
|
||||
const dataSource = app.get(DataSource)
|
||||
|
||||
// Delete in correct order due to foreign key constraints
|
||||
await dataSource.query(
|
||||
`DELETE FROM omnivore.entity_labels WHERE library_item_id = $1`,
|
||||
[testLibraryItemId],
|
||||
)
|
||||
await dataSource.query(
|
||||
`DELETE FROM omnivore.labels WHERE user_id = $1`,
|
||||
[userId],
|
||||
)
|
||||
await dataSource.query(
|
||||
`DELETE FROM omnivore.library_item WHERE user_id = $1`,
|
||||
[userId],
|
||||
)
|
||||
await dataSource.query(`DELETE FROM omnivore.user WHERE id = $1`, [
|
||||
userId,
|
||||
])
|
||||
}
|
||||
|
||||
await app.close()
|
||||
})
|
||||
|
||||
const executeQuery = async (
|
||||
query: string,
|
||||
variables: Record<string, any> = {},
|
||||
) => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/api/graphql')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ query, variables })
|
||||
}
|
||||
|
||||
// ==================== LABEL QUERIES ====================
|
||||
|
||||
describe('Label Queries', () => {
|
||||
it('should return empty array when no labels exist', async () => {
|
||||
const response = await executeQuery(`
|
||||
query {
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
position
|
||||
internal
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.labels).toEqual([])
|
||||
})
|
||||
|
||||
it('should create and retrieve a label', async () => {
|
||||
const createResponse = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
position
|
||||
internal
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Important',
|
||||
color: '#FF5733',
|
||||
description: 'Important articles',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(createResponse.status).toBe(200)
|
||||
expect(createResponse.body.data.createLabel).toMatchObject({
|
||||
name: 'Important',
|
||||
color: '#FF5733',
|
||||
description: 'Important articles',
|
||||
internal: false,
|
||||
})
|
||||
|
||||
const labelId = createResponse.body.data.createLabel.id
|
||||
createdLabelIds.push(labelId)
|
||||
|
||||
// Query single label
|
||||
const queryResponse = await executeQuery(
|
||||
`
|
||||
query GetLabel($id: String!) {
|
||||
label(id: $id) {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: labelId },
|
||||
)
|
||||
|
||||
expect(queryResponse.status).toBe(200)
|
||||
expect(queryResponse.body.data.label).toMatchObject({
|
||||
id: labelId,
|
||||
name: 'Important',
|
||||
color: '#FF5733',
|
||||
})
|
||||
})
|
||||
|
||||
it('should retrieve all labels for a user', async () => {
|
||||
// Create another label
|
||||
const createResponse = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Read Later',
|
||||
color: '#00FF00',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
createdLabelIds.push(createResponse.body.data.createLabel.id)
|
||||
|
||||
// Get all labels
|
||||
const response = await executeQuery(`
|
||||
query {
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
position
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.labels).toHaveLength(2)
|
||||
expect(response.body.data.labels).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Important' }),
|
||||
expect.objectContaining({ name: 'Read Later' }),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== LABEL MUTATIONS ====================
|
||||
|
||||
describe('Label Mutations', () => {
|
||||
it('should create a label with minimal fields', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Minimal Label',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.createLabel).toMatchObject({
|
||||
name: 'Minimal Label',
|
||||
color: '#000000', // Default color
|
||||
description: null,
|
||||
})
|
||||
|
||||
createdLabelIds.push(response.body.data.createLabel.id)
|
||||
})
|
||||
|
||||
it('should validate hex color format', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Invalid Color',
|
||||
color: 'not-a-hex-color',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.errors).toBeDefined()
|
||||
// GraphQL wraps validation errors in Bad Request Exception
|
||||
const errorMessage = response.body.errors[0].message
|
||||
expect(errorMessage).toBeDefined()
|
||||
expect(
|
||||
errorMessage.includes('hex color') ||
|
||||
errorMessage.includes('Bad Request')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should prevent duplicate label names', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Important', // Already created in previous test
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.errors).toBeDefined()
|
||||
expect(response.body.errors[0].message).toContain('already exists')
|
||||
})
|
||||
|
||||
it('should update a label', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation UpdateLabel($id: String!, $input: UpdateLabelInput!) {
|
||||
updateLabel(id: $id, input: $input) {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
id: createdLabelIds[0],
|
||||
input: {
|
||||
name: 'Very Important',
|
||||
color: '#FF0000',
|
||||
description: 'Updated description',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.updateLabel).toMatchObject({
|
||||
id: createdLabelIds[0],
|
||||
name: 'Very Important',
|
||||
color: '#FF0000',
|
||||
description: 'Updated description',
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a label', async () => {
|
||||
// Create a label to delete
|
||||
const createResponse = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'To Delete',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const labelId = createResponse.body.data.createLabel.id
|
||||
|
||||
// Delete the label
|
||||
const deleteResponse = await executeQuery(
|
||||
`
|
||||
mutation DeleteLabel($id: String!) {
|
||||
deleteLabel(id: $id) {
|
||||
success
|
||||
message
|
||||
itemId
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: labelId },
|
||||
)
|
||||
|
||||
expect(deleteResponse.status).toBe(200)
|
||||
expect(deleteResponse.body.data.deleteLabel).toMatchObject({
|
||||
success: true,
|
||||
itemId: labelId,
|
||||
})
|
||||
|
||||
// Verify label is deleted
|
||||
const queryResponse = await executeQuery(
|
||||
`
|
||||
query GetLabel($id: String!) {
|
||||
label(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: labelId },
|
||||
)
|
||||
|
||||
expect(queryResponse.body.data.label).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== LABEL-LIBRARY ITEM ASSOCIATIONS ====================
|
||||
|
||||
describe('Label-Library Item Associations', () => {
|
||||
it('should set labels on a library item', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation SetLibraryItemLabels($itemId: String!, $labelIds: [String!]!) {
|
||||
setLibraryItemLabels(itemId: $itemId, labelIds: $labelIds) {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
itemId: testLibraryItemId,
|
||||
labelIds: [createdLabelIds[0], createdLabelIds[1]],
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.setLibraryItemLabels).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should replace existing labels when setting new ones', async () => {
|
||||
// Set to only one label
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation SetLibraryItemLabels($itemId: String!, $labelIds: [String!]!) {
|
||||
setLibraryItemLabels(itemId: $itemId, labelIds: $labelIds) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
itemId: testLibraryItemId,
|
||||
labelIds: [createdLabelIds[0]],
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.setLibraryItemLabels).toHaveLength(1)
|
||||
expect(response.body.data.setLibraryItemLabels[0].id).toBe(
|
||||
createdLabelIds[0],
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear all labels when setting empty array', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation SetLibraryItemLabels($itemId: String!, $labelIds: [String!]!) {
|
||||
setLibraryItemLabels(itemId: $itemId, labelIds: $labelIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
itemId: testLibraryItemId,
|
||||
labelIds: [],
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.setLibraryItemLabels).toEqual([])
|
||||
})
|
||||
|
||||
it('should return labels with library item query', async () => {
|
||||
// First set some labels
|
||||
await executeQuery(
|
||||
`
|
||||
mutation SetLibraryItemLabels($itemId: String!, $labelIds: [String!]!) {
|
||||
setLibraryItemLabels(itemId: $itemId, labelIds: $labelIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
itemId: testLibraryItemId,
|
||||
labelIds: [createdLabelIds[0]],
|
||||
},
|
||||
)
|
||||
|
||||
// Query library item with labels
|
||||
const response = await executeQuery(
|
||||
`
|
||||
query GetLibraryItem($id: String!) {
|
||||
libraryItem(id: $id) {
|
||||
id
|
||||
title
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: testLibraryItemId },
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.libraryItem.labels).toHaveLength(1)
|
||||
expect(response.body.data.libraryItem.labels[0]).toMatchObject({
|
||||
id: createdLabelIds[0],
|
||||
name: 'Very Important',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== VALIDATION & ERROR HANDLING ====================
|
||||
|
||||
describe('Validation & Error Handling', () => {
|
||||
it('should validate label name length', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'a'.repeat(101), // Exceeds 100 char limit
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.errors).toBeDefined()
|
||||
})
|
||||
|
||||
it('should validate description length', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation CreateLabel($input: CreateLabelInput!) {
|
||||
createLabel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
name: 'Valid Name',
|
||||
description: 'a'.repeat(501), // Exceeds 500 char limit
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.errors).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return error for non-existent label', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
query GetLabel($id: String!) {
|
||||
label(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: randomUUID() },
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.data.label).toBeNull()
|
||||
})
|
||||
|
||||
it('should return error when setting invalid label IDs', async () => {
|
||||
const response = await executeQuery(
|
||||
`
|
||||
mutation SetLibraryItemLabels($itemId: String!, $labelIds: [String!]!) {
|
||||
setLibraryItemLabels(itemId: $itemId, labelIds: $labelIds) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
itemId: testLibraryItemId,
|
||||
labelIds: [randomUUID()],
|
||||
},
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.errors).toBeDefined()
|
||||
expect(response.body.errors[0].message).toContain('not found')
|
||||
})
|
||||
})
|
||||
})
|
||||
1091
packages/api-nest/test/library.e2e-spec.ts
Normal file
1091
packages/api-nest/test/library.e2e-spec.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue