Merge branch 'v1.3-dev' as v1.3.11
[websub-hub] / test-e2e / fake-servers.js
1 /* eslint-disable sonarjs/no-duplicate-string */
2 'use strict';
3
4 /**
5 * For testing, this is a configurable endpoint server.
6 *
7 * Set how a subscriber id responds to a verification GET call
8 * PUT /subscriber/:id/verify?responseCode=xxx&matchChallenge=true
9 * Set how an id responds to a content delivery POST call
10 * PUT /subscriber/:id/content?responseCode=xxx
11 * Remove an id
12 * DELETE /subscriber/:id
13 *
14 * Set how a topic id returns
15 * PUT /topic/:id?statusCode=xxx&content=xxx&contentType=foo/bar
16 */
17
18 const http = require('http');
19 const { Dingus, Enum, Errors } = require('@squeep/api-dingus');
20
21 const subscriberPort = process.env.FAKE_SUBSCRIBER_PORT || 9876;
22 const topicPort = process.env.FAKE_TOPIC_PORT || 9875;
23 const listenAddress = process.env.FAKE_LISTEN_ADDR || '127.0.0.1';
24
25 class TopicFake extends Dingus {
26 constructor() {
27 super(console, {
28 ignoreTrailingSlash: true,
29 });
30 this.topicBehaviors = new Map();
31 this.on(['GET'], '/topic/:id', this.getId.bind(this));
32
33 this.on(['PUT'], '/topic/:id', this.putId.bind(this));
34 this.on(['DELETE'], '/topic/:id', this.deleteId.bind(this));
35 }
36
37 async getId(req, res, ctx) {
38 this.setResponseType(this.responseTypes, req, res, ctx);
39 const behavior = this.topicBehaviors.get(ctx.params.id);
40 if (!behavior) {
41 throw new Errors.ResponseError(Enum.ErrorResponse.NotFound);
42 }
43 if (behavior.contentType) {
44 res.setHeader(Enum.Header.ContentType, behavior.contentType);
45 }
46 res.setHeader('Link', behavior.selfLink + (behavior.hubLink ? `, ${behavior.hubLink}` : ''));
47 res.statusCode = behavior.statusCode;
48 res.end(behavior.content);
49 this.logger.info({ method: req.method, statusCode: res.statusCode, url: req.url });
50 }
51
52 async putId(req, res, ctx) {
53 const id = ctx.params.id;
54 this.setResponseType(this.responseTypes, req, res, ctx);
55 const behavior = {
56 statusCode: ctx.queryParams.statusCode || 200,
57 ...(ctx.queryParams.contentType && { contentType: ctx.queryParams.contentType }),
58 content: ctx.queryParams.content,
59 selfLink: `<http://${listenAddress}:${topicPort}/${id}>; rel="self"`,
60 ...(ctx.queryParams.hubUrl && { hubLink: `<${ctx.queryParams.hubUrl}>; rel="hub"` }),
61 };
62 this.topicBehaviors.set(id, behavior);
63 res.statusCode = 200;
64 res.end();
65 }
66
67 async deleteId(req, res, ctx) {
68 this.setResponseType(this.responseTypes, req, res, ctx);
69 this.topicBehaviors.delete(ctx.params.id);
70 res.statusCode = 200;
71 res.end();
72 }
73
74 } // TopicFake
75
76 class SubscriberFake extends Dingus {
77 constructor() {
78 super(console, {
79 ignoreTrailingSlash: true,
80 });
81 this.verifyBehaviors = new Map();
82 this.contentBehaviors = new Map();
83 this.on(['GET'], '/subscriber/:id', this.getId.bind(this));
84 this.on(['POST'], '/subscriber/:id', this.postId.bind(this));
85
86 this.on(['PUT'], '/subscriber/:id/verify', this.putVerify.bind(this));
87 this.on(['PUT'], '/subscriber/:id/content', this.putContent.bind(this));
88 this.on(['DELETE'], '/subscriber/:id', this.deleteId.bind(this));
89 }
90
91 // eslint-disable-next-line class-methods-use-this
92 parseBody() {
93 // do not parse, just ingest
94 }
95
96 async getId(req, res, ctx) {
97 this.setResponseType(this.responseTypes, req, res, ctx);
98 const behavior = this.verifyBehaviors.get(ctx.params.id);
99 res.statusCode = behavior ? behavior.statusCode : 404;
100 const response = (behavior && behavior.matchChallenge) ? ctx.queryParams['hub.challenge'] : (behavior && behavior.response);
101 res.end(response);
102 this.logger.info({ method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior && behavior.matchChallenge), url: req.url });
103 }
104
105 async postId(req, res, ctx) {
106 this.setResponseType(this.responseTypes, req, res, ctx);
107 await this.ingestBody(req, res, ctx);
108 const behavior = this.contentBehaviors.get(ctx.params.id);
109 res.statusCode = behavior ? behavior.statusCode : 404;
110 if (behavior) {
111 behavior.updated = new Date();
112 behavior.content = ctx.rawBody;
113 }
114 res.end();
115 this.logger.info({ content: behavior && behavior.content, method: req.method, statusCode: res.statusCode, matchChallenge: !!(behavior && behavior.matchChallenge), url: req.url });
116 }
117
118 async putVerify(req, res, ctx) {
119 this.setResponseType(this.responseTypes, req, res, ctx);
120 const behavior = {
121 matchChallenge: ctx.queryParams.matchChallenge === 'true',
122 statusCode: ctx.queryParams.statusCode || 200,
123 };
124 this.verifyBehaviors.set(ctx.params.id, behavior);
125 if (!this.contentBehaviors.get(ctx.params.id)) {
126 this.contentBehaviors.set(ctx.params.id, {
127 statusCode: 200,
128 });
129 }
130 res.statusCode = 200;
131 res.end();
132 }
133
134 async putContent(req, res, ctx) {
135 this.setResponseType(this.responseTypes, req, res, ctx);
136 const behavior = {
137 statusCode: ctx.queryParams.statusCode || 200,
138 };
139 this.contentBehaviors.set(ctx.params.id, behavior);
140 res.statusCode = 200;
141 res.end();
142 }
143
144 async deleteId(req, res, ctx) {
145 this.setResponseType(this.responseTypes, req, res, ctx);
146 this.contentBehaviors.delete(ctx.params.id);
147 this.verifyBehaviors.delete(ctx.params.id);
148 res.statusCode = 200;
149 res.end();
150 }
151
152 } // SubscriberFake
153
154 const subscriberService = new SubscriberFake();
155 http.createServer((req, res) => {
156 subscriberService.dispatch(req, res);
157 }).listen(subscriberPort, listenAddress, (err) => {
158 if (err) {
159 console.error(err);
160 throw err;
161 }
162 console.log(`Fake Subscriber Server started on ${listenAddress}:${subscriberPort}`);
163 });
164
165 const topicService = new TopicFake();
166 http.createServer((req, res) => {
167 topicService.dispatch(req, res);
168 }).listen(topicPort, listenAddress, (err) => {
169 if (err) {
170 console.error(err);
171 throw err;
172 }
173 console.log(`Fake Topic Server started on ${listenAddress}:${topicPort}`);
174 });