thompsonsj / payload-crowdin-sync

Upload and sync localized fields from the default locale to Crowdin.
7 stars 6 forks source link

Add integration tests #40

Closed thompsonsj closed 1 year ago

thompsonsj commented 1 year ago

From the README:

Notes on the current test suite

Integration tests are not ideal: they break a lot of best practice when writing tests. However, they are effective, and this is the reason they are included.

They use the Payload CMS REST API to perform operations on the test server.

The advantages of this approach are that the setup is as similar as possible to production. The test server runs with almost exactly the same configuration as production.

However, there are many disadvantages. Tests should be refactored. The reason for not doing this so far is that it has been difficult to set up preferred alternatives - I experienced a lot of complaints from Babel, Payload, TypeScript, Jest. In particular, the get-port dependency from Payload CMS caused a lot of issues because it uses a require import. This occured when trying to initiate Payload for tests directly. Having said that, Payload CMS tests run fine so maybe further work is needed to emulate that test setup.

Integration tests do not tear down data. This is an unfortunate side effect of running a seperate test server. However, the benefit is that the databse can be inspected after the test is run. Manually deleting the policies, crowdin-files, crowdin-article-directories and crowdin-collection-directories folders is possible, and Payload CMS will recreate those collections once the tests are re-run.

CrowdIn API responses are mocked. Configuration in server.ts detects when this test database is running and provides a mock CrowdIn API service that returns sample data. This is another unfortunate side effect of the test setup - best practice dictates that API responses should never be mocked. See notes on this approach below.

Preferred alternatives:

thompsonsj commented 1 year ago

In progress:

thompsonsj commented 1 year ago

For reference, the previous (awkward) integration tests:

The test logic is still valid - it should be refactored into the new integration test setup.

import axios from 'axios'
import qs from 'qs'
import { devUser } from '../../credentials'

const endpoint = 'http://localhost:3000/api'

const login = async () => {
  const res = await axios.post(`${endpoint}/users/login`, {
    email: devUser.email,
    password: devUser.password,
  })
  return res
}

const retrievePolicy = async (id: string) => {
  // retrieve policy so that afterChange hook modifications are applied
  // and we esnure a depth of 4 (rather than the default of 2)
  const latestPolicy = await axios.get(`${endpoint}/policies/${id}?depth=4`)
  return latestPolicy
}

const retrieveArticleDirectory = async (documentId: string) => {
  const query = {
    owner: {
      equals: {
        value: documentId,
        relationTo: 'policies',
      },
    },
  }

  const stringifiedQuery = qs.stringify({
    where: query
  }, { addQueryPrefix: true });

  const directories = await axios.get(`${endpoint}/crowdin-article-directories/${stringifiedQuery}`)

  if (directories.data.totalDocs === 1){
    return directories.data.docs[0]
  }
  return null
}

const retrieveCrowdinFiles = async (documentId: string) => {
  const query = {
    crowdinArticleDirectory: {
      equals: documentId,
    },
  }

  const stringifiedQuery = qs.stringify({
    where: query
  }, { addQueryPrefix: true });

  const files = await axios.get(`${endpoint}/crowdin-files/${stringifiedQuery}`)

  if (files.data.totalDocs > 0 ){
    return files.data.docs
  }
  return []
}

describe('afterChange hook - on create', () => {
  let token,
  policyCreatedWithTitle,
  policyCreatedWithTitleAndContent,
  policyCreatedWithTitleAndMeta

  beforeAll(async () => {
    const authenticatedUser = await login()
    token = authenticatedUser.data.token

    policyCreatedWithTitle = await axios.post(`${endpoint}/policies`, {
      title: 'Test Policy created with title',
    },
    { headers: {
      Authorization: `JWT ${token}`,
    }})

    policyCreatedWithTitleAndContent = await axios.post(`${endpoint}/policies`, {
      title: 'Test Policy created with title and content',
      content: {
        children: [
          {
            text: "Test content"
          }
        ]
      }
    },
    { headers: {
      Authorization: `JWT ${token}`,
    }})

    // when payload-seo plugin is active
    policyCreatedWithTitleAndMeta = await axios.post(`${endpoint}/policies`, {
      title: 'Test Policy created with title',
      meta: {
        title: 'Test Policy created with title | Teamtailor'
      }
    },
    { headers: {
      Authorization: `JWT ${token}`,
    }})

    //await new Promise(resolve => setTimeout(resolve, 2000))
  })

  afterAll(async () => {
    // how to clear the database?
  })

  describe('on create', () => {
    /*
      There should be a test for creation of the collection
      directory but due to the way tests are set up, there
      isn't a method of emptying the database yet.
    */

    it('should only create one CrowdIn Article Directory', async () => {
      const policy = await retrievePolicy(policyCreatedWithTitle.data.doc.id)
      const crowdinArticleDirectoryId = policy.data.crowdinArticleDirectory.id
      const updatedPolicy = await axios.patch(`${endpoint}/policies/${policy.data.id}`, {
        title: 'Test Policy updated with title',
      },
      { headers: {
        Authorization: `JWT ${token}`,
      }})
      expect(updatedPolicy.data.doc.crowdinArticleDirectory.id).toEqual(crowdinArticleDirectoryId)
    })

    it('should create a "fields" CrowdIn file to include the title field', async () => {
      const policy = await retrievePolicy(policyCreatedWithTitle.data.doc.id)
      const crowdinArticleDirectoryId = policy.data.crowdinArticleDirectory.id
      const crowdinFiles = await retrieveCrowdinFiles(crowdinArticleDirectoryId)

      expect(crowdinFiles.length).toEqual(1)
      expect(crowdinFiles.find(doc => doc.field === 'fields')).not.toEqual(undefined)

      // Additional check to ensure the CrowdIn file has the correct type
      expect(crowdinFiles.find(doc => doc.field === 'fields').type).toEqual('json')
    })

    it('should create a CrowdIn File for the content fields and a "fields" file to include the title field', async () => {
      const policy = await retrievePolicy(policyCreatedWithTitleAndContent.data.doc.id)
      const crowdinArticleDirectoryId = policy.data.crowdinArticleDirectory.id
      const crowdinFiles = await retrieveCrowdinFiles(crowdinArticleDirectoryId)

      expect(crowdinFiles.length).toEqual(2)
      expect(crowdinFiles.find(doc => doc.field === 'fields')).not.toEqual(undefined)
      expect(crowdinFiles.find(doc => doc.field === 'content')).not.toEqual(undefined)

      // Additional check to ensure the CrowdIn file has the correct type
      expect(crowdinFiles.find(doc => doc.field === 'fields').type).toEqual('json')
      expect(crowdinFiles.find(doc => doc.field === 'content').type).toEqual('html')
    })

    it('should create a "fields" CrowdIn File to include the title field and associate the file with a CrowdIn Article Directory', async () => {
      const policy = await retrievePolicy(policyCreatedWithTitle.data.doc.id)
      const crowdinArticleDirectoryId = policy.data.crowdinArticleDirectory.id
      const crowdinFiles = await retrieveCrowdinFiles(crowdinArticleDirectoryId)

      expect(crowdinFiles.find(doc => doc.field === 'fields').crowdinArticleDirectory.id).toEqual(crowdinArticleDirectoryId)
    })

    it('should associate the CrowdIn Article Directory for an article with a CrowdIn Collection Directory', async () => {
      const policy = await retrievePolicy(policyCreatedWithTitle.data.doc.id)

      expect(policy.data.crowdinArticleDirectory.crowdinCollectionDirectory.name).toEqual('policies')
    })

    it('should use the same collection directory for two articles created in the same collection', async () => {
      const policyOne = await retrievePolicy(policyCreatedWithTitle.data.doc.id)
      const policyTwo = await retrievePolicy(policyCreatedWithTitleAndContent.data.doc.id)

      expect(policyOne.data.crowdinArticleDirectory.crowdinCollectionDirectory).toEqual(policyTwo.data.crowdinArticleDirectory.crowdinCollectionDirectory)
    })

    it('should create unique article directories for two articles created in the same collection', async () => {
      const policyOne = await retrievePolicy(policyCreatedWithTitle.data.doc.id)
      const policyTwo = await retrievePolicy(policyCreatedWithTitleAndContent.data.doc.id)

      expect(policyOne.data.crowdinArticleDirectory.id).not.toEqual(policyTwo.data.crowdinArticleDirectory.id)
    })
  })

  describe('on update', () => {
    it('should update the "fields" CrowdIn File if the field has changed', async () => {
      const title = 'Test Policy created with title'
      const policy = await axios.post(`${endpoint}/policies`, {
        title: title,
      },
      { headers: {
        Authorization: `JWT ${token}`,
      }})
      const latestPolicy = await retrievePolicy(policy.data.doc.id)
      const crowdinArticleDirectoryId = latestPolicy.data.crowdinArticleDirectory.id
      const crowdinFilesBefore = await retrieveCrowdinFiles(crowdinArticleDirectoryId)
      const updatedPolicy = await axios.patch(`${endpoint}/policies/${policy.data.doc.id}`, {
        title: `${title} - changed`,
      },
      { headers: {
        Authorization: `JWT ${token}`,
      }})
      // not ideal to wait, but the test server runs separately and
      // we need to be sure all the hooks have copmpleted.
      await new Promise(resolve => setTimeout(resolve, 500))
      const crowdinFilesAfter = await retrieveCrowdinFiles(crowdinArticleDirectoryId)

      expect(crowdinFilesBefore.find(doc => doc.field === 'fields').updatedAt).not.toEqual(crowdinFilesAfter.find(doc => doc.field === 'fields').updatedAt)
    })

    it('should not update the "fields" CrowdIn File if the title field has not changed', async () => {
      const title = 'Test Policy created with title'
      const policy = await axios.post(`${endpoint}/policies`, {
        title: title,
      },
      { headers: {
        Authorization: `JWT ${token}`,
      }})
      const latestPolicy = await retrievePolicy(policy.data.doc.id)
      const crowdinArticleDirectoryId = latestPolicy.data.crowdinArticleDirectory.id
      const crowdinFilesBefore = await retrieveCrowdinFiles(crowdinArticleDirectoryId)
      const updatedPolicy = await axios.patch(`${endpoint}/policies/${policy.data.doc.id}`, {
        title: title,
      },
      { headers: {
        Authorization: `JWT ${token}`,
      }})
      // not ideal to wait, but the test server runs separately and
      // we need to be sure all the hooks have copmpleted.
      await new Promise(resolve => setTimeout(resolve, 500))
      const crowdinFilesAfter = await retrieveCrowdinFiles(crowdinArticleDirectoryId)

      expect(crowdinFilesBefore.find(doc => doc.field === 'fields').updatedAt).toEqual(crowdinFilesAfter.find(doc => doc.field === 'fields').updatedAt)
    })
  })
})

// Todo: TDD of CrowdIn fields not appearing if no localized fields
// Todo: make fields hidden/read only, ensure tests pass
// Todo: documentation in README of unusual test setup

// One more approach! Try with a hasMany relation on crowdin-article-directories :sweat:
thompsonsj commented 1 year ago

Another reference: unit tests from the previous setup:

import { buildCrowdinJsonObject, fieldChanged } from '.'
import { FieldWithName } from '../types'

describe("Function: fieldChanged", () => {
  it ("detects a richText field change on create", () => {
    const before = undefined
    const after = {
      children: [
        {
          text: "Test content"
        }
      ]
    }
    const type = 'richText'
    expect(fieldChanged(before, after, type)).toEqual(true)
  })

  it ("detects a richText field change on update", () => {
    const before = {
      children: [
        {
          text: "Test content before"
        }
      ]
    }
    const after = {
      children: [
        {
          text: "Test content"
        }
      ]
    }
    const type = 'richText'
    expect(fieldChanged(before, after, type)).toEqual(true)
  })

  it ("returns false for equal richText objects", () => {
    const before = {
      children: [
        {
          text: "Test content"
        }
      ]
    }
    const after = before
    const type = 'richText'
    expect(fieldChanged(before, after, type)).toEqual(false)
  })
})

describe("Function: buildCrowdinJsonObject", () => {
  it ("does not include undefined localized fields", () => {
    const doc = {
      id: '638641358b1a140462752076',
      title: 'Test Policy created with title',
      status: 'draft',
      createdAt: '2022-11-29T17:28:21.644Z',
      updatedAt: '2022-11-29T17:28:21.644Z'
    }
    const localizedFields: FieldWithName[] = [
      {
        name: 'title',
        type: 'text'
      },
      {
        name: 'anotherString',
        type: 'text'
      }
    ]
    const expected = {
      title: 'Test Policy created with title',
    }
    expect(buildCrowdinJsonObject(doc, localizedFields)).toEqual(expected)
  })

  it ("includes localized fields", () => {
    const doc = {
      id: '638641358b1a140462752076',
      title: 'Test Policy created with title',
      anotherString: 'An example string',
      status: 'draft',
      createdAt: '2022-11-29T17:28:21.644Z',
      updatedAt: '2022-11-29T17:28:21.644Z'
    }
    const localizedFields: FieldWithName[] = [
      {
        name: 'title',
        type: 'text'
      },
      {
        name: 'anotherString',
        type: 'text'
      }
    ]
    const expected = {
      title: 'Test Policy created with title',
      anotherString: 'An example string',
    }
    expect(buildCrowdinJsonObject(doc, localizedFields)).toEqual(expected)
  })

  it ("includes localized fields and meta @payloadcms/plugin-seo ", () => {
    const doc = {
      id: '638641358b1a140462752076',
      title: 'Test Policy created with title',
      status: 'draft',
      meta: { title: 'Test Policy created with title | Teamtailor' },
      createdAt: '2022-11-29T17:28:21.644Z',
      updatedAt: '2022-11-29T17:28:21.644Z'
    }
    const localizedFields: FieldWithName[] = [
      {
        name: 'title',
        type: 'text'
      }
    ]
    const expected = {
      title: 'Test Policy created with title',
      meta: { title: 'Test Policy created with title | Teamtailor' },
    }
    expect(buildCrowdinJsonObject(doc, localizedFields)).toEqual(expected)
  })

  it ("includes localized fields and removes localization keys from meta @payloadcms/plugin-seo ", () => {
    const doc = {
      id: '638641358b1a140462752076',
      title: 'Test Policy created with title',
      status: 'draft',
      meta: { title: { en: 'Test Policy created with title | Teamtailor' } },
      createdAt: '2022-11-29T17:28:21.644Z',
      updatedAt: '2022-11-29T17:28:21.644Z'
    }
    const localizedFields: FieldWithName[] = [
      {
        name: 'title',
        type: 'text'
      }
    ]
    const expected = {
      title: 'Test Policy created with title',
      meta: { title: 'Test Policy created with title | Teamtailor' },
    }
    expect(buildCrowdinJsonObject(doc, localizedFields)).toEqual(expected)
  })
})