diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 00000000000..48f4b6d7cf4 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,41 @@ +name: Integration Test + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * 1' # weekly on Mondays at 03:00 + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + integration_test: + name: Integration Test + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + + - name: Set node version to 22 + uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 + with: + node-version: 22 + cache: 'pnpm' + + - name: Install deps + run: pnpm install + env: + CYPRESS_INSTALL_BINARY: 0 + + - name: Build + run: pnpm run build + + - name: Integration Test + run: pnpm run integration-test diff --git a/package.json b/package.json index e193e6294da..a0ccb6acc8c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test": "vitest", "test:update-snapshots": "vitest run -u", "coverage": "vitest run --coverage", + "integration-test": "vitest -c vitest.it-config.ts", "cypress": "cypress", "docs:test:e2e:ci": "run-s docs:build:ci docs:test:e2e:run", "docs:test:e2e:run": "run-p --race docs:serve \"cypress run\"", diff --git a/test/integration/modules/image.spec.ts b/test/integration/modules/image.spec.ts new file mode 100644 index 00000000000..0901b99b313 --- /dev/null +++ b/test/integration/modules/image.spec.ts @@ -0,0 +1,119 @@ +/* + * Integration tests for the image methods ensuring that the returned urls work. + */ +import https from 'node:https'; +import { resolve as urlResolve } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { faker } from '../../../src'; + +/** + * Checks that the given address is a working https address. + * + * An address is considered working, if it: + * + * - is a string + * - starts with https + * - is a proper url + * - returns a http-200 (after redirects) + * + * There is a separate unit test file for checking if the returned URL matches the expectations (domain, parameters, etc.). + * + * @param address The address to check. + */ +async function assertWorkingUrl(address: string): Promise { + expect(address).toBeTypeOf('string'); + expect(address).toMatch(/^https:\/\//); + expect(() => new URL(address)).not.toThrow(); + + await expect( + new Promise((resolve, reject) => { + https + .get(address, ({ statusCode, headers: { location } }) => { + if (statusCode == null) { + reject(new Error(`No StatusCode, expected 200`)); + } else if (statusCode === 200) { + resolve(true); + } else if (statusCode >= 300 && statusCode < 400 && location) { + const newAddress = urlResolve(address, location); + assertWorkingUrl(newAddress) + .then(() => resolve(true)) + .catch((error: unknown) => { + reject( + new Error(`Failed to resolve redirect to '${location}'`, { + cause: error, + }) + ); + }); + } else { + reject( + new Error( + `Bad StatusCode ${statusCode} expected 200 for '${location}'` + ) + ); + } + }) + .on('error', (error: unknown) => { + reject(new Error(`Failed to get '${address}'`, { cause: error })); + }); + }) + ).resolves.toBe(true); +} + +describe('image', () => { + describe('avatar', () => { + it('should return a random avatar url', async () => { + const actual = faker.image.avatar(); + await assertWorkingUrl(actual); + }); + }); + + describe('avatarGitHub', () => { + it('should return a random avatar url from GitHub', async () => { + const actual = faker.image.avatarGitHub(); + await assertWorkingUrl(actual); + }); + }); + + describe('url', () => { + it('should return a random image url', async () => { + const actual = faker.image.url(); + await assertWorkingUrl(actual); + }); + + it('should return a random image url with a width', async () => { + const actual = faker.image.url({ width: 100 }); + await assertWorkingUrl(actual); + }); + + it('should return a random image url with a height', async () => { + const actual = faker.image.url({ height: 100 }); + await assertWorkingUrl(actual); + }); + + it('should return a random image url with a width and height', async () => { + const actual = faker.image.url({ width: 128, height: 64 }); + await assertWorkingUrl(actual); + }); + }); + + describe('urlLoremFlickr', () => { + it('should return a random image url from LoremFlickr', async () => { + const actual = faker.image.urlLoremFlickr(); + await assertWorkingUrl(actual); + }); + }); + + describe('urlPicsumPhotos', () => { + it('should return a random image url from PicsumPhotos', async () => { + const actual = faker.image.urlPicsumPhotos(); + await assertWorkingUrl(actual); + }); + }); + + describe('urlPlaceholder', () => { + it('should return a random image url from Placeholder', async () => { + const actual = faker.image.urlPlaceholder(); + await assertWorkingUrl(actual); + }); + }); +}); diff --git a/test/modules/image.spec.ts b/test/modules/image.spec.ts index 8624a9650ff..36be582d8a5 100644 --- a/test/modules/image.spec.ts +++ b/test/modules/image.spec.ts @@ -3,6 +3,25 @@ import { describe, expect, it } from 'vitest'; import { faker } from '../../src'; import { seededTests } from '../support/seeded-runs'; +/** + * Checks that the given address is a valid https address. + * + * An address is considered valid, if it: + * + * - is a string + * - starts with https + * - is a proper url + * + * There is a separate integretation test file for checking if the address is reachable. + * + * @param address The address to check. + */ +function assertValidUrl(address: string): void { + expect(address).toBeTypeOf('string'); + expect(address).toMatch(/^https:\/\//); + expect(() => new URL(address)).not.toThrow(); +} + describe('image', () => { seededTests(faker, 'image', (t) => { t.itEach('avatar', 'avatarGitHub', 'avatarLegacy'); @@ -93,85 +112,78 @@ describe('image', () => { describe('avatar', () => { it('should return a random avatar url', () => { - const avatarUrl = faker.image.avatar(); + const actual = faker.image.avatar(); - expect(avatarUrl).toBeTypeOf('string'); - expect(avatarUrl).toMatch(/^https:\/\//); - expect(() => new URL(avatarUrl)).not.toThrow(); + assertValidUrl(actual); }); }); describe('avatarGitHub', () => { it('should return a random avatar url from GitHub', () => { - const avatarUrl = faker.image.avatarGitHub(); + const actual = faker.image.avatarGitHub(); - expect(avatarUrl).toBeTypeOf('string'); - expect(avatarUrl).toMatch( + expect(actual).toBeTypeOf('string'); + expect(actual).toMatch( /^https:\/\/avatars\.githubusercontent\.com\/u\/\d+$/ ); + assertValidUrl(actual); }); }); describe('avatarLegacy', () => { it('should return a random avatar url from cloudflare-ipfs', () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const avatarUrl = faker.image.avatarLegacy(); + const actual = faker.image.avatarLegacy(); - expect(avatarUrl).toBeTypeOf('string'); - expect(avatarUrl).toMatch( + expect(actual).toBeTypeOf('string'); + expect(actual).toMatch( /^https:\/\/cloudflare-ipfs\.com\/ipfs\/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye\/avatar\/\d{1,4}\.jpg$/ ); + // The links aren't working anymore - there is nothing we can do about it + // assertWebAddress(avatarUrl); }); }); describe('url', () => { it('should return a random image url', () => { - const imageUrl = faker.image.url(); + const actual = faker.image.url(); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch(/^https:\/\//); - expect(() => new URL(imageUrl)).not.toThrow(); + assertValidUrl(actual); }); it('should return a random image url with a width', () => { const width = 100; - const imageUrl = faker.image.url({ width }); + const actual = faker.image.url({ width }); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch(/^https:\/\//); - expect(() => new URL(imageUrl)).not.toThrow(); - expect(imageUrl).include(`${width}`); + assertValidUrl(actual); + expect(actual).include(`${width}`); }); it('should return a random image url with a height', () => { const height = 100; - const imageUrl = faker.image.url({ height }); + const actual = faker.image.url({ height }); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch(/^https:\/\//); - expect(() => new URL(imageUrl)).not.toThrow(); - expect(imageUrl).include(`${height}`); + assertValidUrl(actual); + expect(actual).include(`${height}`); }); it('should return a random image url with a width and height', () => { const width = 128; const height = 64; - const imageUrl = faker.image.url({ width, height }); + const actual = faker.image.url({ width, height }); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch(/^https:\/\//); - expect(() => new URL(imageUrl)).not.toThrow(); - expect(imageUrl).include(`${width}`); - expect(imageUrl).include(`${height}`); + assertValidUrl(actual); + expect(actual).include(`${width}`); + expect(actual).include(`${height}`); }); }); describe('urlLoremFlickr', () => { it('should return a random image url from LoremFlickr', () => { - const imageUrl = faker.image.urlLoremFlickr(); + const actual = faker.image.urlLoremFlickr(); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch( + assertValidUrl(actual); + expect(actual).toMatch( /^https:\/\/loremflickr\.com\/\d+\/\d+\?lock=\d+$/ ); }); @@ -179,10 +191,10 @@ describe('image', () => { describe('urlPicsumPhotos', () => { it('should return a random image url from PicsumPhotos', () => { - const imageUrl = faker.image.urlPicsumPhotos(); + const actual = faker.image.urlPicsumPhotos(); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch( + assertValidUrl(actual); + expect(actual).toMatch( /^https:\/\/picsum\.photos\/seed\/[0-9a-zA-Z]+\/\d+\/\d+(\?(grayscale&?)?(blur=\d+)?)?$/ ); }); @@ -190,10 +202,10 @@ describe('image', () => { describe('urlPlaceholder', () => { it('should return a random image url from Placeholder', () => { - const imageUrl = faker.image.urlPlaceholder(); + const actual = faker.image.urlPlaceholder(); - expect(imageUrl).toBeTypeOf('string'); - expect(imageUrl).toMatch( + assertValidUrl(actual); + expect(actual).toMatch( /^https:\/\/via\.placeholder\.com\/\d+x\d+\/[0-9a-fA-F]{6}\/[0-9a-fA-F]{6}\.[a-z]{3,4}\?text=.+$/ ); }); @@ -201,42 +213,47 @@ describe('image', () => { describe('dataUri', () => { it('should return an image data uri', () => { - const dataUri = faker.image.dataUri(); - expect(dataUri).toMatch(/^data:image\/svg\+xml;/); - expect(dataUri).toSatisfy(isDataURI); + const actual = faker.image.dataUri(); + + expect(actual).toMatch(/^data:image\/svg\+xml;/); + expect(actual).toSatisfy(isDataURI); }); it('should return an uri-encoded image data uri', () => { - const dataUri = faker.image.dataUri({ type: 'svg-uri' }); - expect(dataUri).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); - expect(dataUri).toSatisfy(isDataURI); + const actual = faker.image.dataUri({ type: 'svg-uri' }); + + expect(actual).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); + expect(actual).toSatisfy(isDataURI); }); it('should return a base64 image data uri', () => { - const dataUri = faker.image.dataUri({ type: 'svg-base64' }); - expect(dataUri).toMatch(/^data:image\/svg\+xml;base64,/); - expect(dataUri).toSatisfy(isDataURI); + const actual = faker.image.dataUri({ type: 'svg-base64' }); + + expect(actual).toMatch(/^data:image\/svg\+xml;base64,/); + expect(actual).toSatisfy(isDataURI); }); it('should return an image data uri with fixed size', () => { - const dataUri = faker.image.dataUri({ + const actual = faker.image.dataUri({ width: 200, height: 300, type: 'svg-uri', // required for the regex check }); - expect(dataUri).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); - expect(dataUri).toMatch(/width%3D%22200%22%20height%3D%22300/); - expect(dataUri).toSatisfy(isDataURI); + + expect(actual).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); + expect(actual).toMatch(/width%3D%22200%22%20height%3D%22300/); + expect(actual).toSatisfy(isDataURI); }); it('should return an image data uri with a fixed background color', () => { - const dataUri = faker.image.dataUri({ + const actual = faker.image.dataUri({ color: 'red', type: 'svg-uri', // required for the regex check }); - expect(dataUri).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); - expect(dataUri).toMatch(/fill%3D%22red/); - expect(dataUri).toSatisfy(isDataURI); + + expect(actual).toMatch(/^data:image\/svg\+xml;charset=UTF-8,/); + expect(actual).toMatch(/fill%3D%22red/); + expect(actual).toSatisfy(isDataURI); }); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 94991a6f777..24a9cda3c6f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,8 @@ console.log('VITEST_SEQUENCE_SEED', VITEST_SEQUENCE_SEED); export default defineConfig({ test: { setupFiles: ['test/setup.ts'], + include: ['test/**/*.spec.ts'], + exclude: ['test/integration/**/*.spec.ts'], coverage: { all: true, provider: 'v8', diff --git a/vitest.it-config.ts b/vitest.it-config.ts new file mode 100644 index 00000000000..585b3d8555b --- /dev/null +++ b/vitest.it-config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; +import config from './vitest.config'; + +delete config.test?.coverage; +delete config.test?.typecheck; +delete config.test?.exclude; + +export default defineConfig({ + test: { + ...config.test, + include: ['test/integration/**/*.spec.ts'], + testTimeout: 30000, + }, +});