@vue-skuilder/express 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.env.development +9 -0
  2. package/.prettierignore +4 -0
  3. package/.prettierrc +8 -0
  4. package/.vscode/launch.json +20 -0
  5. package/assets/classroomDesignDoc.js +24 -0
  6. package/assets/courseValidateDocUpdate.js +56 -0
  7. package/assets/get-tagsDesignDoc.json +9 -0
  8. package/babel.config.js +6 -0
  9. package/dist/app.d.ts +6 -0
  10. package/dist/app.d.ts.map +1 -0
  11. package/dist/app.js +194 -0
  12. package/dist/app.js.map +1 -0
  13. package/dist/attachment-preprocessing/index.d.ts +11 -0
  14. package/dist/attachment-preprocessing/index.d.ts.map +1 -0
  15. package/dist/attachment-preprocessing/index.js +146 -0
  16. package/dist/attachment-preprocessing/index.js.map +1 -0
  17. package/dist/attachment-preprocessing/normalize.d.ts +7 -0
  18. package/dist/attachment-preprocessing/normalize.d.ts.map +1 -0
  19. package/dist/attachment-preprocessing/normalize.js +90 -0
  20. package/dist/attachment-preprocessing/normalize.js.map +1 -0
  21. package/dist/client-requests/classroom-requests.d.ts +26 -0
  22. package/dist/client-requests/classroom-requests.d.ts.map +1 -0
  23. package/dist/client-requests/classroom-requests.js +171 -0
  24. package/dist/client-requests/classroom-requests.js.map +1 -0
  25. package/dist/client-requests/course-requests.d.ts +10 -0
  26. package/dist/client-requests/course-requests.d.ts.map +1 -0
  27. package/dist/client-requests/course-requests.js +135 -0
  28. package/dist/client-requests/course-requests.js.map +1 -0
  29. package/dist/client.d.ts +31 -0
  30. package/dist/client.d.ts.map +1 -0
  31. package/dist/client.js +70 -0
  32. package/dist/client.js.map +1 -0
  33. package/dist/couchdb/authentication.d.ts +4 -0
  34. package/dist/couchdb/authentication.d.ts.map +1 -0
  35. package/dist/couchdb/authentication.js +64 -0
  36. package/dist/couchdb/authentication.js.map +1 -0
  37. package/dist/couchdb/index.d.ts +18 -0
  38. package/dist/couchdb/index.d.ts.map +1 -0
  39. package/dist/couchdb/index.js +52 -0
  40. package/dist/couchdb/index.js.map +1 -0
  41. package/dist/design-docs.d.ts +63 -0
  42. package/dist/design-docs.d.ts.map +1 -0
  43. package/dist/design-docs.js +90 -0
  44. package/dist/design-docs.js.map +1 -0
  45. package/dist/logger.d.ts +3 -0
  46. package/dist/logger.d.ts.map +1 -0
  47. package/dist/logger.js +62 -0
  48. package/dist/logger.js.map +1 -0
  49. package/dist/routes/logs.d.ts +3 -0
  50. package/dist/routes/logs.d.ts.map +1 -0
  51. package/dist/routes/logs.js +274 -0
  52. package/dist/routes/logs.js.map +1 -0
  53. package/dist/utils/env.d.ts +10 -0
  54. package/dist/utils/env.d.ts.map +1 -0
  55. package/dist/utils/env.js +38 -0
  56. package/dist/utils/env.js.map +1 -0
  57. package/dist/utils/processQueue.d.ts +39 -0
  58. package/dist/utils/processQueue.d.ts.map +1 -0
  59. package/dist/utils/processQueue.js +175 -0
  60. package/dist/utils/processQueue.js.map +1 -0
  61. package/eslint.config.js +19 -0
  62. package/jest.config.ts +24 -0
  63. package/package.json +74 -0
  64. package/src/app.ts +246 -0
  65. package/src/attachment-preprocessing/index.ts +204 -0
  66. package/src/attachment-preprocessing/normalize.ts +123 -0
  67. package/src/client-requests/classroom-requests.ts +234 -0
  68. package/src/client-requests/course-requests.ts +188 -0
  69. package/src/client.ts +97 -0
  70. package/src/couchdb/authentication.ts +85 -0
  71. package/src/couchdb/index.ts +76 -0
  72. package/src/design-docs.ts +107 -0
  73. package/src/logger.ts +75 -0
  74. package/src/routes/logs.ts +289 -0
  75. package/src/utils/env.ts +51 -0
  76. package/src/utils/processQueue.ts +218 -0
  77. package/test/client.test.ts +144 -0
  78. package/tsconfig.json +27 -0
@@ -0,0 +1,289 @@
1
+ import express, { Request, Response } from 'express';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import rateLimit from 'express-rate-limit';
5
+
6
+ import logger from '../logger.js';
7
+ import process from 'process';
8
+
9
+ const router = express.Router();
10
+
11
+ // Rate limiting middleware for logs routes
12
+ const logsRateLimit = rateLimit({
13
+ windowMs: 15 * 60 * 1000, // 15 minutes
14
+ max: 20, // Limit each IP to 20 requests per windowMs
15
+ message: {
16
+ error: 'Too many log requests from this IP, please try again later.'
17
+ },
18
+ standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
19
+ legacyHeaders: false, // Disable the `X-RateLimit-*` headers
20
+ });
21
+
22
+ // Apply rate limiting to all routes in this router
23
+ router.use(logsRateLimit);
24
+
25
+ // Get list of available log files
26
+ router.get('/', (_req: Request, res: Response) => {
27
+ void (async () => {
28
+ // [ ] add an auth mechanism. Below fcn is based on
29
+ // the CouchDB auth mechanism, forwarded from the web-app (not direct-access of server).
30
+ //
31
+ // const auth = await requestIsAdminAuthenticated(req);
32
+ // if (!auth) {
33
+ // res.status(401).json({ error: 'Unauthorized' });
34
+ // return;
35
+ // }
36
+
37
+ try {
38
+ const logsDir = path.join(process.cwd(), 'logs');
39
+ const files = await fs.readdir(logsDir);
40
+
41
+ // Create HTML content
42
+ const html = `
43
+ <!DOCTYPE html>
44
+ <html>
45
+ <head>
46
+ <title>Log Files</title>
47
+ <style>
48
+ body {
49
+ font-family: Arial, sans-serif;
50
+ margin: 20px;
51
+ }
52
+ h1 {
53
+ color: #333;
54
+ }
55
+ .log-list {
56
+ list-style: none;
57
+ padding: 0;
58
+ }
59
+ .log-list li {
60
+ margin: 10px 0;
61
+ }
62
+ .log-list a {
63
+ color: #0066cc;
64
+ text-decoration: none;
65
+ padding: 5px;
66
+ }
67
+ .log-list a:hover {
68
+ text-decoration: underline;
69
+ }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <h1>Available Log Files</h1>
74
+ <ul class="log-list">
75
+ ${files
76
+ .map(
77
+ (file) => `
78
+ <li>
79
+ <a href="/logs/${file}">${file}</a>
80
+ </li>
81
+ `
82
+ )
83
+ .join('')}
84
+ </ul>
85
+ </body>
86
+ </html>
87
+ `;
88
+
89
+ res.type('html').send(html);
90
+ } catch (error) {
91
+ logger.error('Error reading logs directory:', error);
92
+ res.status(500).json({ error: 'Failed to retrieve log files' });
93
+ }
94
+ })();
95
+ });
96
+
97
+ router.get('/:filename', (req: Request, res: Response) => {
98
+ void (async () => {
99
+ try {
100
+ const filename = req.params.filename;
101
+ // Sanitize filename to prevent directory traversal
102
+ if (filename.includes('..') || !filename.match(/^[a-zA-Z0-9-_.]+$/)) {
103
+ res.status(400).send('<h1>Error</h1><p>Invalid filename</p>');
104
+ return;
105
+ }
106
+
107
+ const logPath = path.join(process.cwd(), 'logs', filename);
108
+ const content = (await fs.readFile(logPath, 'utf-8')).toString();
109
+
110
+ // Parse log lines
111
+ const lines = content
112
+ .split('\n')
113
+ .filter((line) => line.trim())
114
+ .map((line) => {
115
+ try {
116
+ return JSON.parse(line);
117
+ } catch {
118
+ return line;
119
+ }
120
+ });
121
+
122
+ const html = `
123
+ <!DOCTYPE html>
124
+ <html>
125
+ <head>
126
+ <title>Log Viewer - ${filename}</title>
127
+ <style>
128
+ body {
129
+ font-family: Arial, sans-serif;
130
+ margin: 20px;
131
+ max-width: 1200px;
132
+ margin: 0 auto;
133
+ padding: 20px;
134
+ }
135
+ .header {
136
+ display: flex;
137
+ justify-content: space-between;
138
+ align-items: center;
139
+ margin-bottom: 20px;
140
+ }
141
+ .nav-links {
142
+ margin-bottom: 20px;
143
+ }
144
+ .nav-links a {
145
+ color: #0066cc;
146
+ text-decoration: none;
147
+ margin-right: 15px;
148
+ }
149
+ .nav-links a:hover {
150
+ text-decoration: underline;
151
+ }
152
+ .filters {
153
+ margin-bottom: 20px;
154
+ padding: 10px;
155
+ background-color: #f5f5f5;
156
+ border-radius: 4px;
157
+ }
158
+ .filters input, .filters select {
159
+ margin: 5px;
160
+ padding: 5px;
161
+ }
162
+ .log-entry {
163
+ padding: 8px;
164
+ border-bottom: 1px solid #eee;
165
+ font-family: monospace;
166
+ white-space: pre-wrap;
167
+ }
168
+ .log-entry:nth-child(odd) {
169
+ background-color: #f9f9f9;
170
+ }
171
+ .error { color: #d32f2f; }
172
+ .warn { color: #ff9800; }
173
+ .info { color: #2196f3; }
174
+ .debug { color: #4caf50; }
175
+ </style>
176
+ <script>
177
+ function filterLogs() {
178
+ const search = document.getElementById('search').value.toLowerCase();
179
+ const level = document.getElementById('level').value;
180
+ const entries = document.getElementsByClassName('log-entry');
181
+
182
+ Array.from(entries).forEach(entry => {
183
+ const text = entry.textContent.toLowerCase();
184
+ const matchesSearch = !search || text.includes(search);
185
+ const matchesLevel = !level || text.includes('"level":"' + level + '"');
186
+ entry.style.display = (matchesSearch && matchesLevel) ? 'block' : 'none';
187
+ });
188
+ }
189
+ </script>
190
+ </head>
191
+ <body>
192
+ <div class="header">
193
+ <h1>Log Viewer - ${filename}</h1>
194
+ <div class="nav-links">
195
+ <a href="/logs">← Back to Log List</a>
196
+ <a href="/logs/${filename}/raw">View Raw Log →</a>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="filters">
201
+ <input
202
+ type="text"
203
+ id="search"
204
+ placeholder="Search logs..."
205
+ oninput="filterLogs()"
206
+ >
207
+ <select id="level" onchange="filterLogs()">
208
+ <option value="">All Levels</option>
209
+ <option value="error">Error</option>
210
+ <option value="warn">Warn</option>
211
+ <option value="info">Info</option>
212
+ <option value="debug">Debug</option>
213
+ </select>
214
+ </div>
215
+
216
+ <div class="log-content">
217
+ ${lines
218
+ .map((line) => {
219
+ let levelClass = '';
220
+ if (typeof line === 'object' && line.level) {
221
+ levelClass = line.level.toLowerCase();
222
+ }
223
+ return `<div class="log-entry ${levelClass}">
224
+ ${
225
+ typeof line === 'object'
226
+ ? JSON.stringify(line, null, 2)
227
+ : line
228
+ }
229
+ </div>`;
230
+ })
231
+ .join('')}
232
+ </div>
233
+ </body>
234
+ </html>
235
+ `;
236
+
237
+ res.type('html').send(html);
238
+ } catch (error) {
239
+ logger.error(`Error reading log file ${req.params.filename}:`, error);
240
+ res.status(500).send('<h1>Error</h1><p>Failed to retrieve log file</p>');
241
+ }
242
+ })();
243
+ });
244
+
245
+ // Get contents of specific log file
246
+ router.get('/:filename/raw', (req: Request, res: Response) => {
247
+ void (async () => {
248
+ // const auth = await requestIsAdminAuthenticated(req);
249
+ // if (!auth) {
250
+ // res.status(401).json({ error: 'Unauthorized' });
251
+ // return;
252
+ // }
253
+
254
+ try {
255
+ const filename = req.params.filename;
256
+ // Sanitize filename to prevent directory traversal
257
+ if (filename.includes('..') || !filename.match(/^[a-zA-Z0-9-_.]+$/)) {
258
+ res.status(400).json({ error: 'Invalid filename' });
259
+ return;
260
+ }
261
+
262
+ const logPath = path.join(process.cwd(), 'logs', filename);
263
+ const content = (await fs.readFile(logPath, 'utf-8')).toString();
264
+
265
+ // If the client requests JSON format
266
+ if (req.headers.accept === 'application/json') {
267
+ const lines = content
268
+ .split('\n')
269
+ .filter((line) => line.trim())
270
+ .map((line) => {
271
+ try {
272
+ return JSON.parse(line);
273
+ } catch {
274
+ return line;
275
+ }
276
+ });
277
+ res.json(lines);
278
+ } else {
279
+ // Return plain text by default
280
+ res.type('text/plain').send(content);
281
+ }
282
+ } catch (error) {
283
+ logger.error(`Error reading log file ${req.params.filename}:`, error);
284
+ res.status(500).json({ error: 'Failed to retrieve log file' });
285
+ }
286
+ })();
287
+ });
288
+
289
+ export default router;
@@ -0,0 +1,51 @@
1
+ import dotenv from 'dotenv';
2
+ import process from 'process';
3
+
4
+ import { initializeDataLayer } from '@vue-skuilder/db';
5
+ import logger from '../logger.js';
6
+
7
+ dotenv.config({
8
+ path:
9
+ process.argv && process.argv.length == 3
10
+ ? process.argv[2]
11
+ : '.env.development',
12
+ });
13
+
14
+ type Env = {
15
+ COUCHDB_SERVER: string;
16
+ COUCHDB_PROTOCOL: string;
17
+ COUCHDB_ADMIN: string;
18
+ COUCHDB_PASSWORD: string;
19
+ VERSION: string;
20
+ };
21
+
22
+ function getVar(name: string): string {
23
+ if (process.env[name]) {
24
+ return process.env[name];
25
+ } else {
26
+ throw new Error(`${name} not defined in environment`);
27
+ }
28
+ }
29
+
30
+ const env: Env = {
31
+ COUCHDB_SERVER: getVar('COUCHDB_SERVER'),
32
+ COUCHDB_PROTOCOL: getVar('COUCHDB_PROTOCOL'),
33
+ COUCHDB_ADMIN: getVar('COUCHDB_ADMIN'),
34
+ COUCHDB_PASSWORD: getVar('COUCHDB_PASSWORD'),
35
+ VERSION: getVar('VERSION'),
36
+ };
37
+
38
+ initializeDataLayer({
39
+ type: 'pouch',
40
+ options: {
41
+ COUCHDB_PASSWORD: env.COUCHDB_PASSWORD,
42
+ COUCHDB_USERNAME: env.COUCHDB_ADMIN,
43
+ COUCHDB_SERVER_PROTOCOL: env.COUCHDB_PROTOCOL,
44
+ COUCHDB_SERVER_URL: env.COUCHDB_SERVER,
45
+ },
46
+ }).catch((e) => {
47
+ logger.error('Error initializing data layer:', e);
48
+ process.exit(1);
49
+ });
50
+
51
+ export default env;
@@ -0,0 +1,218 @@
1
+ import { IServerRequest } from '@vue-skuilder/common';
2
+ import logger from '../logger.js';
3
+
4
+
5
+ export interface Result {
6
+ status: 'ok' | 'awaiting' | 'warning' | 'error';
7
+ ok: boolean;
8
+ error?: unknown;
9
+ }
10
+
11
+ interface ProcessingFunction<T> {
12
+ (data: T): Promise<Result>;
13
+ }
14
+ interface LabelledRequest<R> {
15
+ id: number;
16
+ request: R;
17
+ }
18
+
19
+ interface FailedRequest<R> extends LabelledRequest<R> {
20
+ result: Result | null;
21
+ error: unknown;
22
+ }
23
+ interface CompletedRequest<R> extends LabelledRequest<R> {
24
+ result: Result;
25
+ }
26
+
27
+ /**
28
+ * This queue executes async prcesses sequentially, waiting
29
+ * for each to complete before launching the next.
30
+ */
31
+ export default class AsyncProcessQueue<
32
+ T extends IServerRequest,
33
+ R extends Result
34
+ > {
35
+ private processRequest: ProcessingFunction<T>;
36
+
37
+ private queue: LabelledRequest<T>[] = [];
38
+ private errors: FailedRequest<T>[] = [];
39
+ private completed: CompletedRequest<T>[] = [];
40
+
41
+ private processing = false;
42
+ private nextID = 0;
43
+
44
+ /**
45
+ * Returns 'complete' if the job is complete, 'error' if the
46
+ * job failed, and the job's position in queue if not yet
47
+ * completed.
48
+ *
49
+ * @param jobID The jobID returned by addRequest
50
+ */
51
+ public jobStatus(jobID: number): 'complete' | 'error' | number {
52
+ let ret: 'complete' | 'error' | number = -1; // Default to -1 if job not found
53
+ this.queue.forEach((req) => {
54
+ if (req.id === jobID) {
55
+ ret = this.queue.indexOf(req);
56
+ }
57
+ });
58
+
59
+ this.completed.forEach((req) => {
60
+ if (req.id === jobID) {
61
+ ret = 'complete';
62
+ }
63
+ });
64
+
65
+ this.errors.forEach((req) => {
66
+ if (req.id === jobID) {
67
+ ret = 'error';
68
+ }
69
+ });
70
+
71
+ return ret;
72
+ }
73
+
74
+ private async recurseGetResult(jobID: number, depth: number): Promise<R> {
75
+ // polling intervals in milliseconds
76
+ logger.info(`Checking job status of job ${jobID}...`);
77
+ const intervals = [100, 200, 400, 800, 1000, 2000, 3000, 5000];
78
+ depth = Math.min(depth, intervals.length - 1);
79
+
80
+ let status: 'complete' | 'error' | number;
81
+
82
+ const p = new Promise((resolve, reject) => {
83
+ setTimeout(
84
+ () => {
85
+ status = this.jobStatus(jobID);
86
+ if (status === 'complete' || status === 'error') {
87
+ resolve(null);
88
+ } else {
89
+ reject();
90
+ }
91
+ },
92
+ intervals[depth]
93
+ );
94
+ });
95
+
96
+ return p
97
+ .then(() => {
98
+ return this.getResult(jobID);
99
+ })
100
+ .catch(() => {
101
+ return this.recurseGetResult(jobID, depth + 1);
102
+ });
103
+ }
104
+
105
+ public async getResult(jobID: number, _depth = 0): Promise<R> {
106
+ const status = this.jobStatus(jobID);
107
+
108
+ if (status === 'complete') {
109
+ const res = this.completed.find((val) => {
110
+ return val.id === jobID;
111
+ });
112
+ if (res) {
113
+ return res.result as R;
114
+ } else {
115
+ return {
116
+ error: 'No result found',
117
+ ok: false,
118
+ status: 'error',
119
+ } as R;
120
+ }
121
+ } else if (status === 'error') {
122
+ const res = this.errors.find((val) => {
123
+ return val.id === jobID;
124
+ });
125
+ if (!res) {
126
+ return {
127
+ error: 'Job failed - no error log found',
128
+ ok: false,
129
+ status: 'error',
130
+ } as R;
131
+ }
132
+
133
+ if (res.result) {
134
+ return res.result as R;
135
+ } else {
136
+ // result is null b/c of an uncaugth exception
137
+ return {
138
+ error: res.error,
139
+ ok: false,
140
+ status: 'error',
141
+ } as R;
142
+ }
143
+ } else {
144
+ return this.recurseGetResult(jobID, 0);
145
+ }
146
+ }
147
+
148
+ public addRequest(req: T): number {
149
+ const id: number = this.nextID++;
150
+
151
+ this.queue.push({
152
+ id: id,
153
+ request: req,
154
+ });
155
+
156
+ if (!this.processing) {
157
+ void this.process();
158
+ }
159
+
160
+ return id;
161
+ }
162
+
163
+ /**
164
+ *
165
+ */
166
+ constructor(processingFcn: ProcessingFunction<T>) {
167
+ this.processRequest = processingFcn;
168
+ }
169
+
170
+ private async process() {
171
+ this.processing = true;
172
+
173
+ while (this.queue.length > 0) {
174
+ const req = this.queue[0];
175
+ logger.info(`Processing ${req.id}`);
176
+
177
+ try {
178
+ const result = await this.processRequest(req.request);
179
+ if (result.ok) {
180
+ this.completed.push({
181
+ id: req.id,
182
+ request: req.request,
183
+ result: result,
184
+ });
185
+ } else {
186
+ if (result) {
187
+ this.errors.push({
188
+ id: req.id,
189
+ request: req.request,
190
+ result: result,
191
+ error: result.error,
192
+ });
193
+ } else {
194
+ this.errors.push({
195
+ id: req.id,
196
+ request: req.request,
197
+ error: 'error',
198
+ result: null,
199
+ });
200
+ }
201
+ }
202
+ } catch (e) {
203
+ this.errors.push({
204
+ id: req.id,
205
+ request: req.request,
206
+ result: null,
207
+ error: e,
208
+ });
209
+ } finally {
210
+ // remove the completed (or errored)
211
+ // request from the queue
212
+ this.queue.shift();
213
+ }
214
+ }
215
+
216
+ this.processing = false;
217
+ }
218
+ }
@@ -0,0 +1,144 @@
1
+ import { ChildProcess, exec } from 'child_process';
2
+ import { CourseConfig } from '../../vue/src/server/types';
3
+ import Client from '../src/client';
4
+ import env from '../src/utils/env';
5
+ import { DataShapeName } from '../../vue/src/enums/DataShapeNames';
6
+ import { abort } from 'process';
7
+
8
+ let serverProcess: ChildProcess;
9
+ const client: Client = new Client('http://localhost:3000');
10
+ const credentials = {
11
+ username: env.COUCHDB_ADMIN,
12
+ password: env.COUCHDB_PASSWORD,
13
+ };
14
+
15
+ beforeAll(async () => {
16
+ serverProcess = exec('yarn serve');
17
+ if (!serverProcess || !serverProcess.stdout) {
18
+ abort();
19
+ }
20
+ serverProcess.stdout.on('data', (data) => {
21
+ console.log(data);
22
+ });
23
+ console.log('Server starting...');
24
+
25
+ const checkServerReady = async () => {
26
+ return new Promise((resolve, reject) => {
27
+ const timeout = setTimeout(() => {
28
+ reject(new Error('Server did not start within 30 seconds'));
29
+ }, 10000);
30
+
31
+ const interval = setInterval(async () => {
32
+ try {
33
+ const response = await client.getVersion();
34
+ if (response) {
35
+ clearTimeout(timeout);
36
+ clearInterval(interval);
37
+ resolve(true);
38
+ }
39
+ } catch (error) {
40
+ // Server not ready yet
41
+ console.log('Server not ready yet');
42
+ }
43
+ }, 1000);
44
+ });
45
+ };
46
+
47
+ return await checkServerReady();
48
+ }, 10_000);
49
+
50
+ test('getVersion', async () => {
51
+ const version = await client.getVersion();
52
+ expect(version).toBe(env.VERSION);
53
+ });
54
+
55
+ test('Course Methods', async () => {
56
+ const testCourse: CourseConfig = createTestCourse();
57
+
58
+ // assert non-existence of test course
59
+ const courses = await client.getCourses();
60
+ expect(courses.find((c) => c.split(' - ')[1] === testCourse.name)).toBeUndefined();
61
+ console.log(`test course not found`);
62
+
63
+ // create test course
64
+ const createResp = await client.createCourse(testCourse, credentials);
65
+ expect(createResp.data?.ok).toBe(true);
66
+ expect(createResp.data?.courseID).not.toBe('');
67
+ console.log(`created test course`);
68
+ const courseID = createResp.data!.courseID;
69
+
70
+ // assert existence of test course
71
+ const courses2 = await client.getCourses();
72
+ expect(courses2.find((c) => c.split(' - ')[1] === testCourse.name)).toBeDefined();
73
+ expect(courses2.find((c) => c.split(' - ')[0] === courseID)).toBeDefined();
74
+ console.log(`found test course`);
75
+
76
+ // delete test course
77
+ const crsClient = client.getCourseClient(courseID);
78
+ await crsClient.deleteCourse(credentials);
79
+ console.log(`requested test course deletion`);
80
+
81
+ // assert non-existence of test course
82
+ const courses3 = await client.getCourses();
83
+ expect(courses3.find((c) => c.split(' - ')[0] === courseID)).toBeUndefined();
84
+ console.log(`test course deleted`);
85
+ });
86
+
87
+ // test('createNote', async () => {
88
+ // const crs = createTestCourse();
89
+ // const createResp = await client.createCourse(crs, credentials);
90
+
91
+ // const crsClient = client.getCourseClient(createResp.data!.courseID);
92
+ // const cfg = await crsClient.getConfig();
93
+
94
+ // for (const k in crs) {
95
+ // expect(cfg[k]).toEqual(crs[k]);
96
+ // }
97
+ // // expect(cfg).toEqual(crs);
98
+ // await crsClient.addData({
99
+ // author: 'test-author',
100
+ // data: 'this is a test',
101
+ // tags: ['test-tag'],
102
+ // courseID: crsClient.id,
103
+ // codeCourse: 'test-code-course-id',
104
+ // shape: {
105
+ // fields: [],
106
+ // name: DataShapeName.Blanks,
107
+ // },
108
+ // });
109
+ // });
110
+
111
+ afterAll(async () => {
112
+ if (!serverProcess || !serverProcess.stdin || !serverProcess.stdout || !serverProcess.stderr) {
113
+ return;
114
+ }
115
+
116
+ // see https://stackoverflow.com/questions/54562879/jest-open-handle-when-using-node-exec/75830272#75830272
117
+ serverProcess.stdin.destroy();
118
+ serverProcess.stdout.destroy();
119
+ serverProcess.stderr.destroy();
120
+
121
+ serverProcess.kill();
122
+ serverProcess.unref();
123
+ });
124
+
125
+ /**
126
+ * @returns a course config with a randomized name
127
+ */
128
+ function createTestCourse(): CourseConfig {
129
+ const salt = Math.random();
130
+ const name = 'test-course-' + salt;
131
+
132
+ const testCourse: CourseConfig = {
133
+ name,
134
+ description: 'test-course',
135
+ admins: ['test-admin'],
136
+ creator: 'test-creator',
137
+ dataShapes: [],
138
+ questionTypes: [],
139
+ deleted: false,
140
+ moderators: [],
141
+ public: true,
142
+ };
143
+ return testCourse;
144
+ }