From 8c40e676f6e7c205a15cac6334e953f4caa0e5b7 Mon Sep 17 00:00:00 2001 From: Patrick Metzdorf Date: Tue, 27 Jan 2026 23:05:38 +0000 Subject: [PATCH 1/4] test: add comprehensive tests for books queries and mutation Adds 8 new tests for the books functionality: - Resolver structure validation - books query (returns array, correct structure, field selection) - book(id) query (single book, non-existent id returns null) - addBook mutation (creates book, created book can be retrieved) Total test count: 18 (was 10) Closes #14 --- src/schema/books/booksSchema.test.js | 221 +++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/schema/books/booksSchema.test.js diff --git a/src/schema/books/booksSchema.test.js b/src/schema/books/booksSchema.test.js new file mode 100644 index 0000000..47fd1e3 --- /dev/null +++ b/src/schema/books/booksSchema.test.js @@ -0,0 +1,221 @@ +import test from 'ava' +import gql from 'graphql-tag' + +import { server } from '../../server.js' +import { bookResolvers } from './index.js' +import { client } from '../../providers/booksDB/dbClient.js' + +// Helper to extract result from Apollo Server 5 response +const getResult = (response) => { + if (response.body.kind !== 'single') { + throw new Error('Expected single result') + } + return response.body.singleResult +} + +// Clean up after all tests +test.after.always(async () => { + await client.$disconnect() +}) + +// ============================================ +// Resolver structure tests +// ============================================ + +test('Book resolvers include all expected functions', t => { + t.truthy(bookResolvers?.Query?.books, 'books resolver missing') + t.true(typeof bookResolvers.Query.books === 'function', 'books resolver is not a function') + + t.truthy(bookResolvers?.Query?.book, 'book resolver missing') + t.true(typeof bookResolvers.Query.book === 'function', 'book resolver is not a function') + + t.truthy(bookResolvers?.Mutation?.addBook, 'addBook resolver missing') + t.true(typeof bookResolvers.Mutation.addBook === 'function', 'addBook resolver is not a function') +}) + +// ============================================ +// Query tests +// ============================================ + +test('books query returns an array', async t => { + const query = gql` + query { + books { + id + title + author + } + } + ` + const response = await server.executeOperation({ query }) + const result = getResult(response) + + t.falsy(result.errors, 'books query returned errors') + t.true(Array.isArray(result.data?.books), 'books should return an array') +}) + +test('books query returns books with correct structure', async t => { + const query = gql` + query { + books { + id + title + author + } + } + ` + const response = await server.executeOperation({ query }) + const result = getResult(response) + + t.falsy(result.errors) + t.true(result.data.books.length >= 1, 'Should have at least one book') + + const book = result.data.books[0] + t.truthy(book.id, 'Book should have an id') + t.truthy(book.title, 'Book should have a title') + t.truthy(book.author, 'Book should have an author') +}) + +test('book query returns a single book by id', async t => { + // First get a valid book id + const listQuery = gql`{ books { id } }` + const listResponse = await server.executeOperation({ query: listQuery }) + const listResult = getResult(listResponse) + const bookId = listResult.data.books[0]?.id + + t.truthy(bookId, 'Need at least one book in DB for this test') + + const query = gql` + query GetBook($id: ID!) { + book(id: $id) { + id + title + author + } + } + ` + const response = await server.executeOperation({ + query, + variables: { id: bookId } + }) + const result = getResult(response) + + t.falsy(result.errors, 'book query returned errors') + t.truthy(result.data?.book, 'book query should return a book') + t.is(result.data.book.id, bookId, 'Returned book should have the requested id') +}) + +test('book query returns null for non-existent id', async t => { + const query = gql` + query GetBook($id: ID!) { + book(id: $id) { + id + title + author + } + } + ` + const response = await server.executeOperation({ + query, + variables: { id: '99999' } + }) + const result = getResult(response) + + t.falsy(result.errors, 'Should not return errors for non-existent book') + t.is(result.data?.book, null, 'Should return null for non-existent book') +}) + +// ============================================ +// Mutation tests +// ============================================ + +test('addBook mutation creates a new book', async t => { + const mutation = gql` + mutation AddBook($title: String, $author: String) { + addBook(title: $title, author: $author) { + id + title + author + } + } + ` + const testTitle = `Test Book ${Date.now()}` + const testAuthor = 'Test Author' + + const response = await server.executeOperation({ + query: mutation, + variables: { title: testTitle, author: testAuthor } + }) + const result = getResult(response) + + t.falsy(result.errors, 'addBook mutation returned errors') + t.truthy(result.data?.addBook, 'addBook should return the created book') + t.truthy(result.data.addBook.id, 'Created book should have an id') + t.is(result.data.addBook.title, testTitle, 'Created book should have the correct title') + t.is(result.data.addBook.author, testAuthor, 'Created book should have the correct author') +}) + +test('addBook mutation - created book can be retrieved', async t => { + const mutation = gql` + mutation AddBook($title: String, $author: String) { + addBook(title: $title, author: $author) { + id + title + author + } + } + ` + const testTitle = `Retrievable Book ${Date.now()}` + const testAuthor = 'Another Author' + + // Create the book + const createResponse = await server.executeOperation({ + query: mutation, + variables: { title: testTitle, author: testAuthor } + }) + const createResult = getResult(createResponse) + const createdId = createResult.data.addBook.id + + // Retrieve it + const query = gql` + query GetBook($id: ID!) { + book(id: $id) { + id + title + author + } + } + ` + const getResponse = await server.executeOperation({ + query, + variables: { id: createdId } + }) + const getResult_ = getResult(getResponse) + + t.falsy(getResult_.errors) + t.is(getResult_.data.book.id, createdId) + t.is(getResult_.data.book.title, testTitle) + t.is(getResult_.data.book.author, testAuthor) +}) + +// ============================================ +// Field selection tests +// ============================================ + +test('books query respects field selection', async t => { + const query = gql` + query { + books { + title + } + } + ` + const response = await server.executeOperation({ query }) + const result = getResult(response) + + t.falsy(result.errors) + const book = result.data.books[0] + t.truthy(book.title, 'Should have title') + t.is(book.id, undefined, 'Should not have id when not requested') + t.is(book.author, undefined, 'Should not have author when not requested') +}) From 20f0a5d158ea13bede39bfa0dfe76eb6d84729e2 Mon Sep 17 00:00:00 2001 From: Patrick Metzdorf Date: Tue, 27 Jan 2026 23:09:07 +0000 Subject: [PATCH 2/4] ci: update GitHub Actions for Node 20+ and modern action versions - Update node.js.yml to test on Node 20.x and 22.x (required by Apollo Server 5 and Prisma 7) - Add npm cache for faster CI - Add prisma generate step before tests - Update actions/checkout and actions/setup-node to v4 - Update codeql-action to v3 --- .github/workflows/codeql-analysis.yml | 24 ++++-------------------- .github/workflows/node.js.yml | 14 ++++++-------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a8bbf10..9ab96c8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,33 +39,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 651187b..69ec3de 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -4,10 +4,6 @@ name: Run tests on: [push, pull_request] -# push: -# branches: [ master ] -# pull_request: -# branches: [ master ] jobs: test: @@ -15,15 +11,17 @@ jobs: strategy: matrix: - node-version: [15.x] + node-version: [20.x, 22.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + # Node 20+ required by Apollo Server 5 and Prisma 7 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + cache: 'npm' - run: npm ci - # - run: npm run build --if-present + - run: npx prisma generate - run: npm test From 6697e92e5f1a1f7ed3c0b1c885136e06f92cb5ae Mon Sep 17 00:00:00 2001 From: Patrick Metzdorf Date: Tue, 27 Jan 2026 23:11:11 +0000 Subject: [PATCH 3/4] test: make book tests self-contained (not dependent on seed data) --- src/schema/books/booksSchema.test.js | 33 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/schema/books/booksSchema.test.js b/src/schema/books/booksSchema.test.js index 47fd1e3..b6f2797 100644 --- a/src/schema/books/booksSchema.test.js +++ b/src/schema/books/booksSchema.test.js @@ -13,6 +13,24 @@ const getResult = (response) => { return response.body.singleResult } +// Helper to create a test book via mutation +const createTestBook = async (title = `Test Book ${Date.now()}`, author = 'Test Author') => { + const mutation = gql` + mutation AddBook($title: String, $author: String) { + addBook(title: $title, author: $author) { + id + title + author + } + } + ` + const response = await server.executeOperation({ + query: mutation, + variables: { title, author } + }) + return getResult(response).data.addBook +} + // Clean up after all tests test.after.always(async () => { await client.$disconnect() @@ -55,6 +73,9 @@ test('books query returns an array', async t => { }) test('books query returns books with correct structure', async t => { + // Ensure at least one book exists + await createTestBook('Structure Test Book', 'Structure Author') + const query = gql` query { books { @@ -77,13 +98,9 @@ test('books query returns books with correct structure', async t => { }) test('book query returns a single book by id', async t => { - // First get a valid book id - const listQuery = gql`{ books { id } }` - const listResponse = await server.executeOperation({ query: listQuery }) - const listResult = getResult(listResponse) - const bookId = listResult.data.books[0]?.id - - t.truthy(bookId, 'Need at least one book in DB for this test') + // Create a book to query + const createdBook = await createTestBook('Single Query Test', 'Query Author') + const bookId = createdBook.id const query = gql` query GetBook($id: ID!) { @@ -103,6 +120,8 @@ test('book query returns a single book by id', async t => { t.falsy(result.errors, 'book query returned errors') t.truthy(result.data?.book, 'book query should return a book') t.is(result.data.book.id, bookId, 'Returned book should have the requested id') + t.is(result.data.book.title, 'Single Query Test') + t.is(result.data.book.author, 'Query Author') }) test('book query returns null for non-existent id', async t => { From 9c05bae761fec2480ecbb163e5fdeb1bd8d153c2 Mon Sep 17 00:00:00 2001 From: Patrick Metzdorf Date: Tue, 27 Jan 2026 23:13:42 +0000 Subject: [PATCH 4/4] ci: simplify to Node 22.x only --- .github/workflows/node.js.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 69ec3de..ba3a30f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -11,8 +11,7 @@ jobs: strategy: matrix: - node-version: [20.x, 22.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + node-version: [22.x] # Node 20+ required by Apollo Server 5 and Prisma 7 steps: