discourse/test/javascripts/helpers/create-pretender.js.es6
Martin Brennan 68d35b14f4 FEATURE: Webauthn authenticator management with 2FA login (Security Keys) (#8099)
Adds 2 factor authentication method via second factor security keys over [web authn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).

Allows a user to authenticate a second factor on login, login-via-email, admin-login, and change password routes. Adds registration area within existing user second factor preferences to register multiple security keys. Supports both external (yubikey) and built-in (macOS/android fingerprint readers).
2019-10-01 19:08:41 -07:00

729 lines
20 KiB
JavaScript

export function parsePostData(query) {
const result = {};
query.split("&").forEach(function(part) {
const item = part.split("=");
const firstSeg = decodeURIComponent(item[0]);
const m = /^([^\[]+)\[(.+)\]/.exec(firstSeg);
const val = decodeURIComponent(item[1]).replace(/\+/g, " ");
if (m) {
let key = m[1];
result[key] = result[key] || {};
result[key][m[2].replace("][", ".")] = val;
} else {
result[firstSeg] = val;
}
});
return result;
}
export function response(code, obj) {
if (typeof code === "object") {
obj = code;
code = 200;
}
return [code, { "Content-Type": "application/json" }, obj];
}
export function success() {
return response({ success: true });
}
const loggedIn = () => !!Discourse.User.current();
const helpers = { response, success, parsePostData };
export let fixturesByUrl;
export default function() {
const server = new Pretender(function() {
// Autoload any `*-pretender` files
Object.keys(requirejs.entries).forEach(e => {
let m = e.match(/^helpers\/([a-z]+)\-pretender$/);
if (m && m[1] !== "create") {
let result = requirejs(e).default.call(this, helpers);
if (m[1] === "fixture") {
fixturesByUrl = result;
}
}
});
this.get("/admin/plugins", () => response({ plugins: [] }));
this.get("/composer_messages", () => response({ composer_messages: [] }));
this.get("/latest.json", () => {
const json = fixturesByUrl["/latest.json"];
if (loggedIn()) {
// Stuff to let us post
json.topic_list.can_create_topic = true;
json.topic_list.draft_key = "new_topic";
json.topic_list.draft_sequence = 1;
}
return response(json);
});
this.get("/c/bug/l/latest.json", () => {
const json = fixturesByUrl["/c/bug/l/latest.json"];
if (loggedIn()) {
// Stuff to let us post
json.topic_list.can_create_topic = true;
json.topic_list.draft_key = "new_topic";
json.topic_list.draft_sequence = 1;
}
return response(json);
});
this.get("/tags", () => {
return response({
tags: [
{
id: "eviltrout",
count: 1
}
]
});
});
this.get("/tags/filter/search", () => {
return response({ results: [{ text: "monkey", count: 1 }] });
});
this.get(`/u/:username/emails.json`, () => {
return response({ email: "eviltrout@example.com" });
});
this.get("/u/eviltrout.json", () => {
const json = fixturesByUrl["/u/eviltrout.json"];
json.user.can_edit = loggedIn();
return response(json);
});
this.get("/u/eviltrout/summary.json", () => {
return response({
user_summary: {
topic_ids: [1234],
replies: [{ topic_id: 1234 }],
links: [{ topic_id: 1234, url: "https://eviltrout.com" }],
most_replied_to_users: [{ id: 333 }],
most_liked_by_users: [{ id: 333 }],
most_liked_users: [{ id: 333 }],
badges: [{ badge_id: 444 }],
top_categories: [
{
id: 1,
name: "bug",
color: "e9dd00",
text_color: "000000",
slug: "bug",
read_restricted: false,
parent_category_id: null,
topic_count: 1,
post_count: 1
}
]
},
badges: [{ id: 444, count: 1 }],
topics: [{ id: 1234, title: "cool title", url: "/t/1234/cool-title" }]
});
});
this.get("/u/eviltrout/invited_count.json", () => {
return response({
counts: { pending: 1, redeemed: 0, total: 0 }
});
});
this.get("/u/eviltrout/invited.json", () => {
return response({ invites: [{ id: 1 }] });
});
this.get("/topics/private-messages/eviltrout.json", () => {
return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]);
});
this.get("/topics/feature_stats.json", () => {
return response({
pinned_in_category_count: 0,
pinned_globally_count: 0,
banner_count: 0
});
});
this.put("/t/34/convert-topic/public", () => {
return response({});
});
this.put("/t/280/make-banner", () => {
return response({});
});
this.put("/t/internationalization-localization/280/status", () => {
return response({
success: "OK",
topic_status_update: null
});
});
this.post("/clicks/track", success);
this.get("/search", request => {
if (request.queryParams.q === "posts") {
return response({
posts: [
{
id: 1234
}
]
});
} else if (request.queryParams.q === "evil") {
return response({
posts: [
{
id: 1234
}
],
tags: [
{
id: 6,
name: "eviltrout"
}
]
});
}
return response({});
});
this.put("/u/eviltrout.json", () => response({ user: {} }));
this.get("/t/280.json", () => response(fixturesByUrl["/t/280/1.json"]));
this.get("/t/34.json", () => response(fixturesByUrl["/t/34/1.json"]));
this.get("/t/280/20.json", () => response(fixturesByUrl["/t/280/1.json"]));
this.get("/t/28830.json", () => response(fixturesByUrl["/t/28830/1.json"]));
this.get("/t/9.json", () => response(fixturesByUrl["/t/9/1.json"]));
this.get("/t/12.json", () => response(fixturesByUrl["/t/12/1.json"]));
this.put("/t/1234/re-pin", success);
this.get("/t/id_for/:slug", () => {
return response({
id: 280,
slug: "internationalization-localization",
url: "/t/internationalization-localization/280"
});
});
this.delete("/t/:id", success);
this.put("/t/:id/recover", success);
this.put("/t/:id/publish", success);
this.get("/404-body", () => {
return [
200,
{ "Content-Type": "text/html" },
"<div class='page-not-found'>not found</div>"
];
});
this.delete("/draft.json", success);
this.post("/draft.json", success);
this.get("/u/:username/staff-info.json", () => response({}));
this.get("/post_action_users", () => {
return response({
post_action_users: [
{
id: 1,
username: "eviltrout",
avatar_template: "/user_avatar/default/eviltrout/{size}/1.png",
username_lower: "eviltrout"
}
]
});
});
this.get("/post_replies", () => {
return response({ post_replies: [{ id: 1234, cooked: "wat" }] });
});
this.get("/post_reply_histories", () => {
return response({ post_reply_histories: [{ id: 1234, cooked: "wat" }] });
});
this.get("/category_hashtags/check", () => {
return response({ valid: [{ slug: "bug", url: "/c/bugs" }] });
});
this.get("/categories_and_latest", () =>
response(fixturesByUrl["/categories_and_latest.json"])
);
this.put("/categories/:category_id", request => {
const category = parsePostData(request.requestBody);
if (category.email_in === "duplicate@example.com") {
return response(422, { errors: ["duplicate email"] });
}
return response({ category });
});
this.get("/draft.json", request => {
if (request.queryParams.draft_key === "new_topic") {
return response(fixturesByUrl["/draft.json"]);
}
return response({});
});
this.get("/drafts.json", () => response(fixturesByUrl["/drafts.json"]));
this.put("/queued_posts/:queued_post_id", function(request) {
return response({ queued_post: { id: request.params.queued_post_id } });
});
this.get("/queued_posts", function() {
return response({
queued_posts: [
{ id: 1, raw: "queued post text", can_delete_user: true }
]
});
});
this.post("/session", function(request) {
const data = parsePostData(request.requestBody);
if (data.password === "correct") {
return response({ username: "eviltrout" });
}
if (data.password === "not-activated") {
return response({
error: "not active",
reason: "not_activated",
sent_to_email: "<small>eviltrout@example.com</small>",
current_email: "<small>current@example.com</small>"
});
}
if (data.password === "not-activated-edit") {
return response({
error: "not active",
reason: "not_activated",
sent_to_email: "eviltrout@example.com",
current_email: "current@example.com"
});
}
if (data.password === "need-second-factor") {
if (data.second_factor_token && data.second_factor_token === "123456") {
return response({ username: "eviltrout" });
}
return response({
error: "Invalid Second Factor",
reason: "invalid_second_factor",
backup_enabled: true,
sent_to_email: "eviltrout@example.com",
current_email: "current@example.com"
});
}
if (data.password === "need-security-key") {
if (data.securityKeyCredential) {
return response({ username: "eviltrout" });
}
return response({
error: "Invalid Security Key",
reason: "invalid_security_key",
backup_enabled: true,
sent_to_email: "eviltrout@example.com",
current_email: "current@example.com"
});
}
return response(400, { error: "invalid login" });
});
this.post("/u/action/send_activation_email", success);
this.put("/u/update-activation-email", success);
this.get("/u/hp.json", function() {
return response({
value: "32faff1b1ef1ac3",
challenge: "61a3de0ccf086fb9604b76e884d75801"
});
});
this.get("/session/csrf", function() {
return response({ csrf: "mgk906YLagHo2gOgM1ddYjAN4hQolBdJCqlY6jYzAYs=" });
});
this.get("/groups/check-name", () => {
return response({ available: true });
});
this.get("/u/check_username", function(request) {
if (request.queryParams.username === "taken") {
return response({ available: false, suggestion: "nottaken" });
}
return response({ available: true });
});
this.post("/u", () => response({ success: true }));
this.get("/login.html", () => [200, {}, "LOGIN PAGE"]);
this.delete("/posts/:post_id", success);
this.put("/posts/:post_id/recover", success);
this.get("/posts/:post_id/expand-embed", success);
this.put("/posts/:post_id", request => {
const data = parsePostData(request.requestBody);
data.post.id = request.params.post_id;
data.post.version = 2;
return response(200, data.post);
});
this.get("/t/403.json", () => response(403, {}));
this.get("/t/404.json", () => response(404, "not found"));
this.get("/t/500.json", () => response(502, {}));
this.put("/t/:slug/:id", request => {
const data = parsePostData(request.requestBody);
return response(200, {
basic_topic: {
id: request.params.id,
title: data.title,
fancy_title: data.title,
slug: request.params.slug
}
});
});
this.get("groups", () => {
return response(200, fixturesByUrl["/groups.json"]);
});
this.get("/groups.json", () => {
return response(200, fixturesByUrl["/groups.json?username=eviltrout"]);
});
this.get("groups/search.json", () => {
return response(200, []);
});
this.get("/topics/groups/discourse.json", () => {
return response(200, fixturesByUrl["/topics/groups/discourse.json"]);
});
this.get("/groups/discourse/mentions.json", () => {
return response(200, fixturesByUrl["/groups/discourse/posts.json"]);
});
this.get("/groups/discourse/messages.json", () => {
return response(200, fixturesByUrl["/groups/discourse/posts.json"]);
});
this.get("/groups/moderators/members.json", () => {
return response(200, fixturesByUrl["/groups/discourse/members.json"]);
});
this.get("/t/:topic_id/posts.json", request => {
const postIds = request.queryParams.post_ids;
const postNumber = parseInt(request.queryParams.post_number);
let posts;
if (postIds) {
posts = postIds.map(p => ({
id: parseInt(p),
post_number: parseInt(p)
}));
} else if (postNumber && request.queryParams.asc === "true") {
posts = _.range(postNumber + 1, postNumber + 6).map(p => ({
id: parseInt(p),
post_number: parseInt(p)
}));
} else if (postNumber && request.queryParams.asc === "false") {
posts = _.range(postNumber - 5, postNumber)
.reverse()
.map(p => ({
id: parseInt(p),
post_number: parseInt(p)
}));
}
return response(200, { post_stream: { posts } });
});
this.get("/posts/:post_id/reply-history.json", () => {
return response(200, [{ id: 2222, post_number: 2222 }]);
});
this.get("/posts/:post_id/reply-ids.json", () => {
return response(200, {
direct_reply_ids: [45],
all_reply_ids: [45, 100]
});
});
this.post("/user_badges", () =>
response(200, fixturesByUrl["/user_badges"])
);
this.delete("/user_badges/:badge_id", success);
this.post("/posts", function(request) {
const data = parsePostData(request.requestBody);
if (data.title === "this title triggers an error") {
return response(422, { errors: ["That title has already been taken"] });
}
if (data.raw === "enqueue this content please") {
return response(200, {
success: true,
action: "enqueued",
pending_post: {
id: 1234,
raw: data.raw
}
});
}
return response(200, {
success: true,
action: "create_post",
post: {
id: 12345,
topic_id: 280,
topic_slug: "internationalization-localization"
}
});
});
this.post("/topics/timings", () => response(200, {}));
const siteText = { id: "site.test", value: "Test McTest" };
const overridden = {
id: "site.overridden",
value: "Overridden",
overridden: true
};
this.get("/admin/users/list/active.json", request => {
let store = [
{
id: 1,
username: "eviltrout",
email: "<small>eviltrout@example.com</small>"
},
{
id: 3,
username: "discobot",
email: "<small>discobot_email</small>"
}
];
const showEmails = request.queryParams.show_emails;
if (showEmails === "false") {
store = store.map(item => {
delete item.email;
return item;
});
}
const ascending = request.queryParams.ascending;
const order = request.queryParams.order;
if (order) {
store = store.sort(function(a, b) {
return a[order] - b[order];
});
}
if (ascending) {
store = store.reverse();
}
return response(200, store);
});
this.get("/admin/users/list/suspect.json", () => {
return response(200, [
{
id: 2,
username: "sam",
email: "<small>sam@example.com</small>"
}
]);
});
this.get("/admin/customize/site_texts", request => {
if (request.queryParams.overridden) {
return response(200, { site_texts: [overridden] });
} else {
return response(200, { site_texts: [siteText, overridden] });
}
});
this.get("/admin/customize/site_texts/:key", () =>
response(200, { site_text: siteText })
);
this.delete("/admin/customize/site_texts/:key", () =>
response(200, { site_text: siteText })
);
this.put("/admin/customize/site_texts/:key", request => {
const result = parsePostData(request.requestBody);
result.id = request.params.key;
result.can_revert = true;
return response(200, { site_text: result });
});
this.get("/tag_groups", () => response(200, { tag_groups: [] }));
this.get("/admin/users/1234.json", () => {
return response(200, {
id: 1234,
username: "regular"
});
});
this.get("/admin/users/1.json", () => {
return response(200, {
id: 1,
username: "eviltrout",
admin: true
});
});
this.get("/admin/users/2.json", () => {
return response(200, {
id: 2,
username: "sam",
admin: true
});
});
this.post("/admin/users/:user_id/generate_api_key", success);
this.delete("/admin/users/:user_id/revoke_api_key", success);
this.delete("/admin/users/:user_id.json", () =>
response(200, { deleted: true })
);
this.post("/admin/badges", success);
this.delete("/admin/badges/:id", success);
this.get("/admin/logs/staff_action_logs.json", () => {
return response(200, {
staff_action_logs: [],
extras: { user_history_actions: [] }
});
});
this.get("/admin/logs/watched_words", () => {
return response(200, fixturesByUrl["/admin/logs/watched_words.json"]);
});
this.delete("/admin/logs/watched_words/:id.json", success);
this.post("/admin/logs/watched_words.json", request => {
const result = parsePostData(request.requestBody);
result.id = new Date().getTime();
return response(200, result);
});
this.get("/admin/logs/search_logs.json", () => {
return response(200, [
{ term: "foobar", searches: 35, click_through: 6, unique: 16 }
]);
});
this.get("/admin/logs/search_logs/term.json", () => {
return response(200, {
term: {
type: "search_log_term",
title: "Search Count",
term: "ruby",
data: [{ x: "2017-07-20", y: 2 }]
}
});
});
this.post("/uploads/lookup-metadata", () => {
return response(200, {
imageFilename: "somefile.png",
imageFilesize: "10 KB",
imageWidth: "1",
imageHeight: "1"
});
});
this.get("/inline-onebox", request => {
if (
request.queryParams.urls.includes(
"http://www.example.com/has-title.html"
)
) {
return [
200,
{ "Content-Type": "application/html" },
'{"inline-oneboxes":[{"url":"http://www.example.com/has-title.html","title":"This is a great title"}]}'
];
}
});
this.get("/onebox", request => {
if (
request.queryParams.url === "http://www.example.com/has-title.html" ||
request.queryParams.url ===
"http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html"
) {
return [
200,
{ "Content-Type": "application/html" },
'<aside class="onebox"><article class="onebox-body"><h3><a href="http://www.example.com/article.html">An interesting article</a></h3></article></aside>'
];
}
if (request.queryParams.url === "http://www.example.com/no-title.html") {
return [
200,
{ "Content-Type": "application/html" },
'<aside class="onebox"><article class="onebox-body"><p>No title</p></article></aside>'
];
}
if (request.queryParams.url.indexOf("/internal-page.html") > -1) {
return [
200,
{ "Content-Type": "application/html" },
'<aside class="onebox"><article class="onebox-body"><h3><a href="/internal-page.html">Internal Page 4 U</a></h3></article></aside>'
];
}
return [404, { "Content-Type": "application/html" }, ""];
});
});
server.prepareBody = function(body) {
if (body && typeof body === "object") {
return JSON.stringify(body);
}
return body;
};
server.unhandledRequest = function(verb, path) {
const error =
"Unhandled request in test environment: " + path + " (" + verb + ")";
window.console.error(error);
throw error;
};
server.checkPassthrough = request =>
request.requestHeaders["Discourse-Script"];
return server;
}