diff --git a/.github/workflows/monitor-notifier.yml b/.github/workflows/monitor-notifier.yml index 6b71441..fde141c 100644 --- a/.github/workflows/monitor-notifier.yml +++ b/.github/workflows/monitor-notifier.yml @@ -1,4 +1,4 @@ -name: monitor-notifier +name: provider-monitor on: workflow_run: @@ -8,7 +8,7 @@ on: workflow_dispatch: jobs: - monitor-failures: + send-error-notification: runs-on: ubuntu-latest steps: @@ -21,4 +21,4 @@ jobs: -F "user=${{ secrets.PUSHOVER_USER_KEY }}" \ -F "title=❌ Test Workflow Failed" \ -F "message=Workflow '${{ github.event.workflow_run.name }}' on Branch '${{ github.event.workflow_run.head_branch }}' failed." \ - https://api.pushover.net/1/messages.json + https://api.pushover.net/1/messages.json \ No newline at end of file diff --git a/.github/workflows/run-provider-test.yml b/.github/workflows/run-provider-test.yml index ceaff7d..249c6fc 100644 --- a/.github/workflows/run-provider-test.yml +++ b/.github/workflows/run-provider-test.yml @@ -3,12 +3,12 @@ name: test on: push: tags: - - '*' - branches: [ main ] + - "*" + branches: [main] pull_request: - branches: [ main ] + branches: [main] schedule: - - cron: '0 6 * * *' # Daily at 6 AM UTC + - cron: "0 6 * * *" # Daily at 6 AM UTC workflow_dispatch: jobs: @@ -41,7 +41,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: 'npm' + cache: "npm" cache-dependency-path: server/package-lock.json - name: Debug files @@ -65,4 +65,33 @@ jobs: libgdk-pixbuf2.0-0 - name: Run Puppeteer test - run: npm test + id: run-tests + run: | + npm test + + - name: Upload provider status + if: always() + uses: actions/upload-artifact@v4 + with: + name: provider-status + path: ./server/test/provider/status.json + + update-gist: + needs: test + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./server + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + - name: Download provider status artifact + uses: actions/download-artifact@v4 + with: + name: provider-status + path: ./server/test/provider + - name: Push provider status to Gist + env: + GIST_ID: ${{ secrets.GIST_ID }} + GIST_TOKEN: ${{ secrets.GIST_TOKEN }} + run: node ./test/update-gist.js diff --git a/.gitignore b/.gitignore index 2fc9e1c..4b00b55 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ dist/ coverage/ jest-output/ test-results/ +server/test/provider/status.json \ No newline at end of file diff --git a/server/test/provider/providers.test.js b/server/test/provider/providers.test.js index 2325393..a08578a 100644 --- a/server/test/provider/providers.test.js +++ b/server/test/provider/providers.test.js @@ -1,10 +1,16 @@ import { expect } from 'chai'; +import fs from 'fs'; +import path from "path"; +import { fileURLToPath } from "url"; +import Listing from '../../models/Listing.js'; import { getAvailableProviders } from '../../provider/index.js'; import * as similarityCache from '../../services/runtime/similarity-check/similarityCache.js'; import { mockJobData } from '../mocks/mockJob.js'; import { get } from '../mocks/mockNotification.js'; import { logObject, mockJobRuntime, providerConfig, validateListings } from '../utils.js'; -import Listing from '../../models/Listing.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const DEBUG = false; const providerToRun = []; // define which providers to run or leave empty for all @@ -16,8 +22,13 @@ describe('#Provider Integration Tests', () => { ) ); + const platformStatus = {}; + after(() => { similarityCache.stopCacheCleanup(); + const filePath = path.join(__dirname, "status.json"); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(platformStatus, null, 2)); }); // test each provider @@ -32,80 +43,89 @@ describe('#Provider Integration Tests', () => { const JobRuntime = await mockJobRuntime(); const runtime = new JobRuntime(provider, job, provider.metaInformation.id, []); - const listings = await runtime.execute(); - - // --- Listings --- - expect(listings) - .to.be.an('array') - .that.is.not.empty; - - const listingExpectedFields = Object.keys(provider.config.crawlFields) - - validateListings( - DEBUG, - listings, - (listing) => { - - // validate mogoose schema - const doc = new Listing(listing); - const validationError = doc.validateSync(); - if (validationError) { - const ignoredFields = ["jobId"]; - const fieldErrors = Object.keys(validationError.errors).filter( - (field) => !ignoredFields.includes(field) - ); - - expect(fieldErrors, `Schema validation failed for fields: ${fieldErrors.join(", ")}`).to.be.empty; - } - - // validate fields - expect(listing).to.include.keys(listingExpectedFields); - - listingExpectedFields.forEach((field) => { - expect(listing[field], field).to.exist; - }) - - // validate urls - const baseUrl = provider.metaInformation.baseUrl; - const imageBaseUrl = provider.metaInformation.imageBaseUrl || baseUrl; - if (listing.imageUrl) expect(listing.imageUrl).to.include(new URL(imageBaseUrl).hostname); - if (listing.url) expect(listing.url).to.include(new URL(baseUrl).hostname); - }, - 0.3, - `Listings (${providerId})` - ); - - // --- Notification --- - const notificationObj = get(); - if (DEBUG) { - logObject(`Notification Object (${providerId})`, notificationObj); // DEBUG + let listings; + let testPassed = false; + + try { + listings = await runtime.execute(); + + // --- Listings --- + expect(listings) + .to.be.an('array') + .that.is.not.empty; + + const listingExpectedFields = Object.keys(provider.config.crawlFields); + + validateListings( + DEBUG, + listings, + (listing) => { + // validate mogoose schema + const doc = new Listing(listing); + const validationError = doc.validateSync(); + if (validationError) { + const ignoredFields = ["jobId"]; + const fieldErrors = Object.keys(validationError.errors).filter( + (field) => !ignoredFields.includes(field) + ); + expect(fieldErrors, `Schema validation failed for fields: ${fieldErrors.join(", ")}`).to.be.empty; + } + + // validate fields + expect(listing).to.include.keys(listingExpectedFields); + + listingExpectedFields.forEach((field) => { + expect(listing[field], field).to.exist; + }); + + const baseUrl = provider.metaInformation.baseUrl; + const imageBaseUrl = provider.metaInformation.imageBaseUrl || baseUrl; + if (listing.imageUrl) expect(listing.imageUrl).to.include(new URL(imageBaseUrl).hostname); + if (listing.url) expect(listing.url).to.include(new URL(baseUrl).hostname); + }, + 0.3, + `Listings (${providerId})` + ); + + // --- Notification --- + const notificationObj = get(); + if (DEBUG) { + logObject(`Notification Object (${providerId})`, notificationObj); // DEBUG + } + + expect(notificationObj) + .to.be.an('object') + .that.has.all.keys(['serviceName', 'listings', 'job']); + + expect(notificationObj.serviceName).to.equal(provider.metaInformation.name); + + expect(notificationObj.listings) + .to.be.an('array') + .that.is.not.empty; + + validateListings( + DEBUG, + notificationObj.listings, + (notify) => { + listingExpectedFields.forEach((field) => { + expect(notify[field]).to.exist; + }); + const baseUrl = provider.metaInformation.baseUrl; + const imageBaseUrl = provider.metaInformation.imageBaseUrl || baseUrl; + if (notify.imageUrl) expect(notify.imageUrl).to.include(new URL(imageBaseUrl).hostname); + if (notify.url) expect(notify.url).to.include(new URL(baseUrl).hostname); + }, + 0.3, + `Notifications (${providerId})` + ); + + testPassed = true; + } catch (err) { + testPassed = false; + throw err; + } finally { + platformStatus[providerId] = testPassed; } - - expect(notificationObj) - .to.be.an('object') - .that.has.all.keys(['serviceName', 'listings', 'job']); - - expect(notificationObj.serviceName).to.equal(provider.metaInformation.name); - - expect(notificationObj.listings) - .to.be.an('array') - .that.is.not.empty; - - validateListings( - DEBUG, - notificationObj.listings, - (notify) => { - listingExpectedFields.forEach((field) => { - expect(notify[field]).to.exist; - }) - const baseUrl = provider.metaInformation.baseUrl; - const imageBaseUrl = provider.metaInformation.imageBaseUrl || baseUrl; - if (notify.imageUrl) expect(notify.imageUrl).to.include(new URL(imageBaseUrl).hostname); - if (notify.url) expect(notify.url).to.include(new URL(baseUrl).hostname); - }, - 0.3, - `Notifications (${providerId})` - ); }); }); } diff --git a/server/test/update-gist.js b/server/test/update-gist.js new file mode 100644 index 0000000..8a9dd84 --- /dev/null +++ b/server/test/update-gist.js @@ -0,0 +1,48 @@ +import fs from "fs"; +import path from "path"; + +const GIST_ID = process.env.GIST_ID; +const GIST_TOKEN = process.env.GIST_TOKEN; + +if (!GIST_TOKEN) { + throw new Error("GIST_TOKEN not set"); +} + +const statusPath = path.resolve("test/provider/status.json"); +const platformStatus = JSON.parse(fs.readFileSync(statusPath, "utf8")); + +let md = "## Plattformstatus\n\n| Plattform | Status |\n|----------|--------|\n"; +for (const [platform, ok] of Object.entries(platformStatus)) { + const color = ok ? "green" : "red"; + const text = ok ? "ok" : "failed"; + md += `| ${platform} | ![](https://img.shields.io/badge/${text}-${color}) |\n`; +} + +const body = { + files: { + "SNOOP_PROVIDER_STATUS.md": { content: md } + } +}; + +const url = GIST_ID + ? `https://api.github.com/gists/${GIST_ID}` + : `https://api.github.com/gists`; + +const method = GIST_ID ? "PATCH" : "POST"; + +fetch(url, { + method, + headers: { + Authorization: `token ${GIST_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(body) +}) + .then(res => res.json()) + .then(json => { + console.log("Gist updated:", json.html_url); + }) + .catch(err => { + console.error(err); + process.exit(1); + });