• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GEWIS / sudosos-backend / 26083912867

19 May 2026 07:51AM UTC coverage: 87.549% (-0.2%) from 87.783%
26083912867

Pull #918

github

web-flow
Merge 550518fe0 into 9dd74ee61
Pull Request #918: Background task queue (replaces BullMQ) with admin API, WebSocket updates, and failed-task health signal

3968 of 4633 branches covered (85.65%)

Branch coverage included in aggregate %.

498 of 563 new or added lines in 16 files covered. (88.45%)

110 existing lines in 13 files now uncovered.

20607 of 23437 relevant lines covered (87.93%)

840.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.33
/src/controller/task-controller.ts
1
/**
1✔
2
 *  SudoSOS back-end API service.
3
 *  Copyright (C) 2026 Study association GEWIS
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU Affero General Public License as published
7
 *  by the Free Software Foundation, either version 3 of the License, or
8
 *  (at your option) any later version.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU Affero General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU Affero General Public License
16
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 *
18
 *  @license
19
 */
1✔
20

21
/**
1✔
22
 * @module tasks
23
 */
1✔
24

25
import { Response } from 'express';
26
import log4js, { Logger } from 'log4js';
27
import BaseController, { BaseControllerOptions } from './base-controller';
28
import Policy from './policy';
29
import { RequestWithToken } from '../middleware/token-middleware';
30
import { parseRequestPagination, toResponse } from '../helpers/pagination';
31
import TaskService from '../service/task-service';
32
import Task, { TaskStatus } from '../entity/task';
33
import { TaskResponse } from './response/task-response';
34

35
const VALID_STATUSES = new Set<string>(Object.values(TaskStatus));
1✔
36

37
export default class TaskController extends BaseController {
1✔
38
  private logger: Logger = log4js.getLogger('TaskController');
1✔
39

40
  public constructor(options: BaseControllerOptions) {
1✔
41
    super(options);
2✔
42
    this.configureLogger(this.logger);
2✔
43
  }
2✔
44

45
  /**
1✔
46
   * @inheritdoc
47
   */
1✔
48
  public getPolicy(): Policy {
1✔
49
    return {
2✔
50
      '/': {
2✔
51
        GET: {
2✔
52
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Task', ['*']),
2✔
53
          handler: this.listTasks.bind(this),
2✔
54
        },
2✔
55
      },
2✔
56
      '/stats': {
2✔
57
        GET: {
2✔
58
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Task', ['*']),
2✔
59
          handler: this.getStats.bind(this),
2✔
60
        },
2✔
61
      },
2✔
62
      '/:id(\\d+)': {
2✔
63
        GET: {
2✔
64
          policy: async (req) => this.roleManager.can(req.token.roles, 'get', 'all', 'Task', ['*']),
2✔
65
          handler: this.getTask.bind(this),
2✔
66
        },
2✔
67
      },
2✔
68
      '/:id(\\d+)/retry': {
2✔
69
        POST: {
2✔
70
          policy: async (req) => this.roleManager.can(req.token.roles, 'update', 'all', 'Task', ['*']),
2✔
71
          handler: this.retryTask.bind(this),
2✔
72
        },
2✔
73
      },
2✔
74
    };
2✔
75
  }
2✔
76

77
  public static asTaskResponse(task: Task): TaskResponse {
1✔
78
    return TaskService.asTaskResponse(task);
4✔
79
  }
4✔
80

81
  /**
1✔
82
   * GET /tasks
83
   * @summary List background tasks, most recent first.
84
   * @operationId listTasks
85
   * @tags tasks - Operations of the task controller
86
   * @security JWT
87
   * @param {integer} take.query - How many tasks the endpoint should return.
88
   * @param {integer} skip.query - How many tasks should be skipped (pagination).
89
   * @param {string} status.query - Filter by status (pending, processing, completed, failed). Comma-separated for multiple.
90
   * @param {string} type.query - Filter by task type.
91
   * @return {PaginatedTaskResponse} 200 - The matching tasks.
92
   * @return {string} 400 - Validation error.
93
   * @return {string} 500 - Internal server error.
94
   */
1✔
95
  public async listTasks(req: RequestWithToken, res: Response): Promise<void> {
1✔
96
    this.logger.trace('List tasks by', req.token.user);
3✔
97

98
    let take: number;
3✔
99
    let skip: number;
3✔
100
    try {
3✔
101
      ({ take, skip } = parseRequestPagination(req));
3✔
102
    } catch (e) {
3!
NEW
103
      res.status(400).send((e as Error).message);
×
NEW
104
      return;
×
NEW
105
    }
×
106

107
    const statusParam = typeof req.query.status === 'string' ? req.query.status : undefined;
3✔
108
    let statuses: TaskStatus[] | undefined;
3✔
109
    if (statusParam) {
3✔
110
      const split = statusParam.split(',').map((s) => s.trim()).filter(Boolean);
2✔
111
      const invalid = split.filter((s) => !VALID_STATUSES.has(s));
2✔
112
      if (invalid.length > 0) {
2✔
113
        res.status(400).send(`Invalid status value(s): ${invalid.join(', ')}`);
1✔
114
        return;
1✔
115
      }
1✔
116
      statuses = split as TaskStatus[];
1✔
117
    }
1✔
118

119
    const typeParam = typeof req.query.type === 'string' ? req.query.type : undefined;
3!
120

121
    try {
3✔
122
      const [tasks, count] = await TaskService.getTasks(
3✔
123
        { status: statuses, type: typeParam },
3✔
124
        { take, skip },
3✔
125
      );
126
      res.json(toResponse(tasks.map(TaskController.asTaskResponse), count, { take, skip }));
2✔
127
    } catch (error) {
3!
NEW
128
      this.logger.error('Could not list tasks:', error);
×
NEW
129
      res.status(500).json('Internal server error.');
×
NEW
130
    }
×
131
  }
3✔
132

133
  /**
1✔
134
   * GET /tasks/{id}
135
   * @summary Get a single task by id.
136
   * @operationId getTask
137
   * @tags tasks - Operations of the task controller
138
   * @security JWT
139
   * @param {integer} id.path.required - The id of the task.
140
   * @return {TaskResponse} 200 - The requested task.
141
   * @return {string} 404 - Task not found.
142
   * @return {string} 500 - Internal server error.
143
   */
1✔
144
  public async getTask(req: RequestWithToken, res: Response): Promise<void> {
1✔
145
    const id = parseInt(req.params.id, 10);
2✔
146
    try {
2✔
147
      const task = await TaskService.getTask(id);
2✔
148
      if (!task) {
2✔
149
        res.status(404).send('Task not found.');
1✔
150
        return;
1✔
151
      }
1✔
152
      res.json(TaskController.asTaskResponse(task));
1✔
153
    } catch (error) {
2!
NEW
154
      this.logger.error('Could not load task:', error);
×
NEW
155
      res.status(500).json('Internal server error.');
×
NEW
156
    }
×
157
  }
2✔
158

159
  /**
1✔
160
   * POST /tasks/{id}/retry
161
   * @summary Reset a failed task so it gets picked up again.
162
   * @operationId retryTask
163
   * @tags tasks - Operations of the task controller
164
   * @security JWT
165
   * @param {integer} id.path.required - The id of the task to retry.
166
   * @return {TaskResponse} 200 - The updated task.
167
   * @return {string} 400 - The task was not in a retryable state.
168
   * @return {string} 404 - Task not found.
169
   * @return {string} 500 - Internal server error.
170
   */
1✔
171
  public async retryTask(req: RequestWithToken, res: Response): Promise<void> {
1✔
172
    const id = parseInt(req.params.id, 10);
2✔
173
    try {
2✔
174
      const task = await TaskService.retry(id);
2✔
175
      if (!task) {
2!
NEW
176
        res.status(404).send('Task not found.');
×
NEW
177
        return;
×
NEW
178
      }
✔
179
      res.json(TaskController.asTaskResponse(task));
1✔
180
    } catch (error) {
1✔
181
      const message = (error as Error)?.message ?? 'Internal server error.';
1!
182
      if (message.includes('not in failed state')) {
1✔
183
        res.status(400).send(message);
1✔
184
        return;
1✔
185
      }
1!
NEW
186
      this.logger.error('Could not retry task:', error);
×
NEW
187
      res.status(500).json('Internal server error.');
×
NEW
188
    }
×
189
  }
2✔
190

191
  /**
1✔
192
   * GET /tasks/stats
193
   * @summary Aggregate counts of tasks by status.
194
   * @operationId getTaskStats
195
   * @tags tasks - Operations of the task controller
196
   * @security JWT
197
   * @return {TaskStatsResponse} 200 - Status counts.
198
   * @return {string} 500 - Internal server error.
199
   */
1✔
200
  public async getStats(req: RequestWithToken, res: Response): Promise<void> {
1✔
201
    try {
1✔
202
      res.json(await TaskService.getStats());
1✔
203
    } catch (error) {
1!
NEW
204
      this.logger.error('Could not get task stats:', error);
×
NEW
205
      res.status(500).json('Internal server error.');
×
NEW
206
    }
×
207
  }
1✔
208
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc