Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/monitor-notifier.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: monitor-notifier
name: provider-monitor

on:
workflow_run:
Expand All @@ -8,7 +8,7 @@ on:
workflow_dispatch:

jobs:
monitor-failures:
send-error-notification:
runs-on: ubuntu-latest

steps:
Expand All @@ -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
41 changes: 35 additions & 6 deletions .github/workflows/run-provider-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ dist/
coverage/
jest-output/
test-results/
server/test/provider/status.json
168 changes: 94 additions & 74 deletions server/test/provider/providers.test.js
Original file line number Diff line number Diff line change
@@ -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
Expand 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
Expand All @@ -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})`
);
});
});
}
Expand Down
48 changes: 48 additions & 0 deletions server/test/update-gist.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading