blob: a0bc2747f91ff164567578a27cae9991e2a84a86 [file] [log] [blame]
Marc Kupietz93d7f702025-06-27 15:41:48 +02001const https = require('https');
Marc Kupietzd0ee97e2025-12-04 15:46:14 +01002const crypto = require('crypto');
Marc Kupietz490b0532024-09-05 09:36:21 +02003const puppeteer = require('puppeteer-extra');
4puppeteer.use(require('puppeteer-extra-plugin-user-preferences')({
5 userPrefs: {
6 safebrowsing: {
7 enabled: false,
8 enhanced: false
9 }
10 }
11}));
Marc Kupietz55fc3162022-12-04 16:25:49 +010012const chai = require('chai');
Marc Kupietz4c5a7a52022-12-04 16:56:30 +010013const { afterEach } = require('mocha');
Marc Kupietz55fc3162022-12-04 16:25:49 +010014const assert = chai.assert;
15const should = chai.should();
Marc Kupietz7f1666a2024-07-12 18:35:31 +020016var slack = null;
Marc Kupietz55fc3162022-12-04 16:25:49 +010017
Marc Kupietz0f6c54d2022-12-03 15:32:40 +010018const KORAP_URL = process.env.KORAP_URL || "http://localhost:64543";
Marc Kupietzbfb23012025-06-03 15:47:10 +020019const KORAP_LOGIN = 'KORAP_USERNAME' in process.env ? process.env.KORAP_USERNAME : 'KORAP_LOGIN' in process.env ? process.env.KORAP_LOGIN : "user2"
20const KORAP_PWD = process.env.KORAP_PWD || process.env.KORAP_PASSWORD || "password2";
Marc Kupietz26982382022-12-04 19:02:57 +010021const KORAP_QUERIES = process.env.KORAP_QUERIES || 'geht, [orth=geht & cmc/pos=VVFIN]'
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +020022const KORAP_MIN_TOKENS_IN_CORPUS = parseInt(process.env.KORAP_MIN_TOKENS_IN_CORPUS || "100000", 10);
Marc Kupietz5a73a4d2022-12-04 14:09:58 +010023const korap_rc = require('../lib/korap_rc.js').new(KORAP_URL)
24
Marc Kupietz7f1666a2024-07-12 18:35:31 +020025const slack_webhook = process.env.SLACK_WEBHOOK_URL;
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010026const nc_talk_url = process.env.NC_TALK_URL || 'https://cloud.ids-mannheim.de';
27const nc_talk_conversation = process.env.NC_TALK_CONVERSATION;
28const nc_talk_secret = process.env.NC_TALK_SECRET;
Marc Kupietz4d335a32024-09-04 16:13:48 +020029
Marc Kupietz7f1666a2024-07-12 18:35:31 +020030if (slack_webhook) {
31 slack = require('slack-notify')(slack_webhook);
32}
33
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010034// Function to send message to Nextcloud Talk
Marc Kupietz65dea512025-12-04 17:55:57 +010035async function sendToNextcloudTalk(message, silent = false, screenshotPath = null) {
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010036 if (!nc_talk_conversation || !nc_talk_secret) {
37 return;
38 }
39
40 try {
41 const axios = require('axios');
Marc Kupietz65dea512025-12-04 17:55:57 +010042 const fs = require('fs');
43 const sharp = require('sharp');
44
45 let fullMessage = message;
46 const MAX_MESSAGE_LENGTH = 32000; // Nextcloud Talk message size limit
47
48 // If a screenshot path is provided, try to embed it
49 if (screenshotPath && fs.existsSync(screenshotPath)) {
50 try {
51 // First, try to resize the image to reduce size
52 const resizedBuffer = await sharp(screenshotPath)
53 .resize(800, null, { // Resize to max width of 800px, maintain aspect ratio
54 withoutEnlargement: true,
55 fit: 'inside'
56 })
57 .png({ quality: 80, compressionLevel: 9 })
58 .toBuffer();
59
60 const base64Image = resizedBuffer.toString('base64');
61 const dataUri = `data:image/png;base64,${base64Image}`;
62 const messageWithImage = `${message}\n\n![Screenshot](${dataUri})`;
63
64 // Check if the message with image fits within the limit
65 if (messageWithImage.length <= MAX_MESSAGE_LENGTH) {
66 fullMessage = messageWithImage;
67 console.log(`Screenshot will be embedded (message size: ${messageWithImage.length} chars)`);
68 } else {
69 console.log(`Screenshot too large (${messageWithImage.length} chars), sending text-only notification`);
70 fullMessage = `${message}\n\n_Screenshot available locally but too large to embed (${Math.round(messageWithImage.length / 1024)}KB)_`;
71 }
72 } catch (imageError) {
73 console.error('Failed to process screenshot for Nextcloud Talk:', imageError.message);
74 fullMessage = `${message}\n\n_Screenshot available locally but could not be processed_`;
75 }
76 }
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010077
78 // Generate random header and signature
79 const randomHeader = crypto.randomBytes(32).toString('hex');
Marc Kupietz65dea512025-12-04 17:55:57 +010080 const messageToSign = randomHeader + fullMessage;
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010081 const signature = crypto.createHmac('sha256', nc_talk_secret)
82 .update(messageToSign)
83 .digest('hex');
84
85 // Send the message
86 await axios.post(
87 `${nc_talk_url}/ocs/v2.php/apps/spreed/api/v1/bot/${nc_talk_conversation}/message`,
88 {
Marc Kupietz65dea512025-12-04 17:55:57 +010089 message: fullMessage,
Marc Kupietzd0ee97e2025-12-04 15:46:14 +010090 silent: silent
91 },
92 {
93 headers: {
94 'Content-Type': 'application/json',
95 'Accept': 'application/json',
96 'OCS-APIRequest': 'true',
97 'X-Nextcloud-Talk-Bot-Random': randomHeader,
98 'X-Nextcloud-Talk-Bot-Signature': signature
99 }
100 }
101 );
102 console.log('Message sent to Nextcloud Talk successfully');
103 } catch (error) {
104 console.error('Failed to send message to Nextcloud Talk:', error.message);
105 }
106}
107
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100108function ifConditionIt(title, condition, test) {
Marc Kupietz55fc3162022-12-04 16:25:49 +0100109 return condition ? it(title, test) : it.skip(title + " (skipped)", test)
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100110}
Marc Kupietzc4077822022-12-03 15:32:40 +0100111
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100112describe('Running KorAP UI end-to-end tests on ' + KORAP_URL, () => {
Marc Kupietzc4077822022-12-03 15:32:40 +0100113
Marc Kupietz93d7f702025-06-27 15:41:48 +0200114 let browser;
115 let page;
116
117
Marc Kupietzc4077822022-12-03 15:32:40 +0100118 before(async () => {
Marc Kupietz69e02802023-11-08 14:37:22 +0100119 browser = await puppeteer.launch({
Marc Kupietzbfb23012025-06-03 15:47:10 +0200120 headless: "shell",
121 args: [
122 '--no-sandbox',
123 '--disable-setuid-sandbox',
124 '--disable-dev-shm-usage',
125 '--disable-accelerated-2d-canvas',
126 '--no-first-run',
127 '--no-zygote',
128 '--disable-gpu'
129 ]
Marc Kupietz69e02802023-11-08 14:37:22 +0100130 })
Marc Kupietzc4077822022-12-03 15:32:40 +0100131 page = await browser.newPage()
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100132 await page.setViewport({
Marc Kupietz9e0f5192025-03-09 12:12:16 +0100133 width: 1980,
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100134 height: 768,
135 deviceScaleFactor: 1,
Marc Kupietz964e7772025-06-03 15:02:30 +0200136 });
Marc Kupietz93d7f702025-06-27 15:41:48 +0200137
Marc Kupietzc4077822022-12-03 15:32:40 +0100138 })
139
Marc Kupietz93d7f702025-06-27 15:41:48 +0200140 after(async function() {
141 if (browser && typeof browser.close === 'function') {
142 await browser.close();
143 }
Marc Kupietzc4077822022-12-03 15:32:40 +0100144 })
145
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100146 afterEach(async function () {
147 if (this.currentTest.state == "failed") {
Marc Kupietz65dea512025-12-04 17:55:57 +0100148 // Only take screenshot if it's not one of the initial connectivity/SSL tests
149 const initialTestTitles = [
150 'should be reachable',
151 'should have a valid SSL certificate'
152 ];
153 let screenshotPath = null;
154
155 if (!initialTestTitles.includes(this.currentTest.title) && page) {
156 screenshotPath = "failed_" + this.currentTest.title.replaceAll(/[ &\/]/g, "_") + '.png';
157 await page.screenshot({ path: screenshotPath });
158 }
159
160 // Send notification to Slack
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200161 if (slack) {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200162 try {
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200163 slack.alert({
164 text: `🚨 Test on ${KORAP_URL} failed: *${this.currentTest.title}*`,
165 attachments: [{
166 color: 'danger',
167 fields: [{
168 title: 'Failed Test',
169 value: this.currentTest.title,
170 short: false
171 }, {
172 title: 'URL',
173 value: KORAP_URL,
174 short: true
175 }]
176 }]
177 });
178 } catch (slackError) {
179 console.error('Failed to send notification to Slack:', slackError.message);
180 }
181 }
182
Marc Kupietz65dea512025-12-04 17:55:57 +0100183 // Upload screenshot to Slack if available
184 if (screenshotPath) {
Marc Kupietz93d7f702025-06-27 15:41:48 +0200185 const slackToken = process.env.SLACK_TOKEN;
186 if (slackToken) {
187 try {
188 const { WebClient } = require('@slack/web-api');
Marc Kupietz65dea512025-12-04 17:55:57 +0100189 const fs = require('fs');
190 const web = new WebClient(slackToken);
Marc Kupietz93d7f702025-06-27 15:41:48 +0200191 const channelId = process.env.SLACK_CHANNEL_ID || 'C07CM4JS48H';
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200192
Marc Kupietz93d7f702025-06-27 15:41:48 +0200193 const result = await web.files.uploadV2({
194 channel_id: channelId,
195 file: fs.createReadStream(screenshotPath),
196 filename: screenshotPath,
197 title: `Screenshot: ${this.currentTest.title}`,
198 initial_comment: `📸 Screenshot of failed test: ${this.currentTest.title} on ${KORAP_URL}`
199 });
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200200
Marc Kupietz93d7f702025-06-27 15:41:48 +0200201 } catch (uploadError) {
202 console.error('Failed to upload screenshot to Slack:', uploadError.message);
203 }
Marc Kupietz8f7c2042025-06-24 09:55:03 +0200204 }
Marc Kupietz7f1666a2024-07-12 18:35:31 +0200205 }
Marc Kupietz65dea512025-12-04 17:55:57 +0100206
207 // Send notification to Nextcloud Talk with screenshot
208 if (nc_talk_conversation && nc_talk_secret) {
209 try {
210 const message = `🚨 Test on ${KORAP_URL} failed: **${this.currentTest.title}**`;
211 await sendToNextcloudTalk(message, false, screenshotPath);
212 } catch (ncError) {
213 console.error('Failed to send notification to Nextcloud Talk:', ncError.message);
214 }
215 }
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100216 }
Marc Kupietz964e7772025-06-03 15:02:30 +0200217 })
Marc Kupietz4c5a7a52022-12-04 16:56:30 +0100218
Marc Kupietz93d7f702025-06-27 15:41:48 +0200219 it('should be reachable', function (done) {
220 let doneCalled = false;
221 const url = new URL(KORAP_URL);
222 const httpModule = url.protocol === 'https:' ? https : require('http');
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100223
Marc Kupietz93d7f702025-06-27 15:41:48 +0200224 const req = httpModule.request({
225 method: 'HEAD',
226 hostname: url.hostname,
227 port: url.port || (url.protocol === 'https:' ? 443 : 80),
228 path: url.pathname,
229 timeout: 5000
230 }, res => {
231 if (!doneCalled) {
232 doneCalled = true;
233 if (res.statusCode >= 200 && res.statusCode < 400) {
234 done();
235 } else {
236 done(new Error(`Server is not reachable. Status code: ${res.statusCode}`));
237 }
238 }
239 });
240 req.on('timeout', () => {
241 if (!doneCalled) {
242 doneCalled = true;
243 req.destroy();
244 done(new Error('Request to server timed out.'));
245 }
246 });
247 req.on('error', err => {
248 if (!doneCalled) {
249 doneCalled = true;
250 done(err);
251 }
252 });
253 req.end();
254 });
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100255
Marc Kupietz93d7f702025-06-27 15:41:48 +0200256 it('should have a valid SSL certificate', function (done) {
257 let doneCalled = false;
258 const url = new URL(KORAP_URL);
259 if (url.protocol !== 'https:') {
260 return this.skip();
261 }
262 const req = https.request({
263 method: 'HEAD',
264 hostname: url.hostname,
265 port: url.port || 443,
266 path: url.pathname,
267 timeout: 5000
268 }, res => {
269 if (!doneCalled) {
270 doneCalled = true;
271 const cert = res.socket.getPeerCertificate();
272 if (cert && cert.valid_to) {
273 const validTo = new Date(cert.valid_to);
274 if (validTo > new Date()) {
275 done();
276 } else {
277 done(new Error(`SSL certificate expired on ${validTo.toDateString()}`));
278 }
279 } else if (res.socket.isSessionReused()){
280 done();
281 }
282 else {
283 done(new Error('Could not retrieve SSL certificate information.'));
284 }
285 }
286 });
287 req.on('timeout', () => {
288 if (!doneCalled) {
289 doneCalled = true;
290 req.destroy();
291 done(new Error('Request to server timed out.'));
292 }
293 });
294 req.on('error', err => {
295 if (!doneCalled) {
296 doneCalled = true;
297 if (err.code === 'CERT_HAS_EXPIRED') {
298 done(new Error('SSL certificate has expired.'));
299 } else {
300 done(err);
301 }
302 }
303 });
304 req.end();
305 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100306
Marc Kupietz93d7f702025-06-27 15:41:48 +0200307 describe('UI Tests', function() {
Marc Kupietzc4077822022-12-03 15:32:40 +0100308
Marc Kupietz93d7f702025-06-27 15:41:48 +0200309 before(function() {
310 // Check the state of the parent suite's tests
311 const initialTests = this.test.parent.parent.tests;
312 if (initialTests[0].state === 'failed' || initialTests[1].state === 'failed') {
313 this.skip();
314 }
315 });
Marc Kupietzc4077822022-12-03 15:32:40 +0100316
Marc Kupietz93d7f702025-06-27 15:41:48 +0200317
Marc Kupietzc8ffb2b2025-06-12 16:44:23 +0200318
Marc Kupietz93d7f702025-06-27 15:41:48 +0200319 it('KorAP UI is up and running', async function () {
320 try {
321 await page.goto(KORAP_URL, { waitUntil: 'domcontentloaded' });
322 await page.waitForSelector("#q-field", { visible: true });
323 const query_field = await page.$("#q-field")
324 assert.isNotNull(query_field, "#q-field not found. Kalamar not running?");
325 } catch (error) {
326 throw new Error(`Failed to load KorAP UI or find query field: ${error.message}`);
327 }
Marc Kupietz0f6c54d2022-12-03 15:32:40 +0100328 })
Marc Kupietz5a73a4d2022-12-04 14:09:58 +0100329
Marc Kupietzc4077822022-12-03 15:32:40 +0100330
Marc Kupietz93d7f702025-06-27 15:41:48 +0200331 ifConditionIt('Login into KorAP with incorrect credentials fails',
332 KORAP_LOGIN != "",
333 (async () => {
334 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD + "*")
335 login_result.should.be.false
336 }))
337
338 ifConditionIt('Login into KorAP with correct credentials succeeds',
339 KORAP_LOGIN != "",
340 (async () => {
341 const login_result = await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD)
342 login_result.should.be.true
343 }))
344
345 it('Can turn glimpse off',
346 (async () => {
347 await korap_rc.assure_glimpse_off(page)
348 }))
349
350 it('Corpus statistics show sufficient tokens',
351 (async () => {
352 const tokenCount = await korap_rc.check_corpus_statistics(page, KORAP_MIN_TOKENS_IN_CORPUS);
353 console.log(`Found ${tokenCount} tokens in corpus, minimum required: ${KORAP_MIN_TOKENS_IN_CORPUS}`);
354 tokenCount.should.be.above(KORAP_MIN_TOKENS_IN_CORPUS - 1,
355 `Corpus should have at least ${KORAP_MIN_TOKENS_IN_CORPUS} tokens, but found ${tokenCount}`);
356 })).timeout(90000)
357
358 describe('Running searches that should have hits', () => {
359
360 before(async () => { await korap_rc.login(page, KORAP_LOGIN, KORAP_PWD) })
361
362 KORAP_QUERIES.split(/[;,] */).forEach((query, i) => {
363 it('Search for "' + query + '" has hits',
364 (async () => {
365 await korap_rc.assure_glimpse_off(page)
366 const hits = await korap_rc.search(page, query)
367 hits.should.be.above(0)
368 })).timeout(20000)
369 })
370 })
371
372 ifConditionIt('Logout works',
373 KORAP_LOGIN != "",
374 (async () => {
375 const logout_result = await korap_rc.logout(page)
376 logout_result.should.be.true
377 })).timeout(15000)
378 });
379});