mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(api): allow redirection with message (#50525)
Co-authored-by: Muhammed Mustafa <MuhammedElruby@gmail.com>
This commit is contained in:
committed by
GitHub
parent
962f45475c
commit
65239ee68b
+3
-2
@@ -5,8 +5,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
"@fastify/middie": "8.3",
|
||||
"@fastify/csrf-protection": "6.3.0",
|
||||
"@fastify/middie": "8.3",
|
||||
"@fastify/session": "^10.1.1",
|
||||
"@fastify/swagger": "^8.3.1",
|
||||
"@fastify/swagger-ui": "^1.5.0",
|
||||
@@ -17,7 +17,8 @@
|
||||
"fastify": "4.17.0",
|
||||
"fastify-auth0-verify": "^1.0.0",
|
||||
"fastify-plugin": "^4.3.0",
|
||||
"nodemon": "2.0.22"
|
||||
"nodemon": "2.0.22",
|
||||
"query-string": "^7.1.3"
|
||||
},
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import qs from 'query-string';
|
||||
|
||||
import redirectWithMessage from './redirect-with-message';
|
||||
|
||||
async function setupServer() {
|
||||
const fastify = Fastify();
|
||||
await fastify.register(redirectWithMessage);
|
||||
return fastify;
|
||||
}
|
||||
|
||||
const isString = (value: unknown): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
describe('redirectWithMessage plugin', () => {
|
||||
it('should decorate reply object with redirectWithMessage method', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const fastify = await setupServer();
|
||||
|
||||
fastify.get('/', (_req, reply) => {
|
||||
expect(reply).toHaveProperty('redirectWithMessage');
|
||||
expect(reply.redirectWithMessage).toBeInstanceOf(Function);
|
||||
return { foo: 'bar' };
|
||||
});
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
});
|
||||
|
||||
expect(res.statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
describe('redirectWithMessage method', () => {
|
||||
let fastify: FastifyInstance;
|
||||
beforeEach(async () => {
|
||||
fastify = await setupServer();
|
||||
});
|
||||
|
||||
it('should redirect to the first argument', async () => {
|
||||
fastify.get('/', (_req, reply) => {
|
||||
return reply.redirectWithMessage('/target', {
|
||||
type: 'info',
|
||||
content: 'foo'
|
||||
});
|
||||
});
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
});
|
||||
|
||||
expect(res.headers.location).toMatch(/^\/target/);
|
||||
expect(res.statusCode).toEqual(302);
|
||||
});
|
||||
|
||||
it('should convert the second argument into a query string', async () => {
|
||||
fastify.get('/', (_req, reply) => {
|
||||
return reply.redirectWithMessage('/target', {
|
||||
type: 'info',
|
||||
content: 'foo'
|
||||
});
|
||||
});
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
});
|
||||
|
||||
expect(res.headers.location).toMatch(/^\/target\?messages=info/);
|
||||
});
|
||||
|
||||
it('should encode the message twice when creating the query string', async () => {
|
||||
const expectedMessage = { danger: ['foo bar'] };
|
||||
|
||||
fastify.get('/', (_req, reply) => {
|
||||
return reply.redirectWithMessage('/target', {
|
||||
type: 'danger',
|
||||
content: 'foo bar'
|
||||
});
|
||||
});
|
||||
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
});
|
||||
if (!isString(res.headers.location))
|
||||
throw new Error('Location is not a string');
|
||||
const { search } = new URL(res.headers.location, 'http://localhost');
|
||||
|
||||
// The query string itself is encoded:
|
||||
const { messages } = qs.parse(search, { arrayFormat: 'index' });
|
||||
if (!isString(messages)) throw new Error('Messages is not a string');
|
||||
|
||||
// As is the message embedded in it:
|
||||
expect(qs.parse(messages, { arrayFormat: 'index' })).toEqual(
|
||||
expectedMessage
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FastifyPluginCallback, type FastifyReply } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
// TODO: (POST MVP)use node's querystring and just JSON stringify the message.
|
||||
// No need for query-string on either side.
|
||||
|
||||
import qs from 'query-string';
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyReply {
|
||||
redirectWithMessage: typeof redirectWithMessage;
|
||||
}
|
||||
}
|
||||
|
||||
type Message = {
|
||||
type: 'info' | 'danger' | 'success' | 'errors';
|
||||
content: string;
|
||||
};
|
||||
|
||||
type MessageQuery = {
|
||||
info?: string[];
|
||||
danger?: string[];
|
||||
success?: string[];
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
// The client expects a message like { info: ['foo'] }, { danger: ['bar'] } etc.
|
||||
const prepareMessage = (message: Message): MessageQuery => ({
|
||||
[message.type]: [message.content]
|
||||
});
|
||||
|
||||
function redirectWithMessage(
|
||||
this: FastifyReply,
|
||||
url: string,
|
||||
message: Message
|
||||
) {
|
||||
return this.redirect(
|
||||
`${url}?${qs.stringify(
|
||||
{
|
||||
messages: qs.stringify(prepareMessage(message), {
|
||||
arrayFormat: 'index'
|
||||
})
|
||||
},
|
||||
{ arrayFormat: 'index' }
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const plugin: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
fastify.decorateReply('redirectWithMessage', redirectWithMessage);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
export default fp(plugin);
|
||||
Generated
+3
@@ -213,6 +213,9 @@ importers:
|
||||
nodemon:
|
||||
specifier: 2.0.22
|
||||
version: 2.0.22
|
||||
query-string:
|
||||
specifier: ^7.1.3
|
||||
version: 7.1.3
|
||||
devDependencies:
|
||||
'@fastify/type-provider-typebox':
|
||||
specifier: 3.2.0
|
||||
|
||||
Reference in New Issue
Block a user