Test server, including GraphQL endpoint

This commit is contained in:
Jordan Eldredge 2022-03-09 21:40:01 -08:00
parent 4bdb243933
commit 50fde5c4d3
7 changed files with 563 additions and 7 deletions

View file

@ -9,8 +9,8 @@ module.exports = {
// TODO: Add these as we can...
"/packages/webamp/",
// TODO: Fix config improt so that this can work.
"/packages/skin-database/",
"/packages/webamp-modern/src/__tests__/integration*",
],
testEnvironment: "jsdom",
setupFiles: ["<rootDir>/packages/skin-database/jest-setup.js"],
};

View file

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Query.fetch_skin_by_md5 (debug data) 1`] = `
Object {
"fetch_skin_by_md5": Object {
"archive_files": Array [
Object {
"date": "1995-12-17T11:24:00.000Z",
"file_md5": "a_fake_file_md5",
"filename": null,
"is_directory": false,
"size": null,
"skin": Object {
"md5": "a_fake_md5",
},
"text_content": null,
"url": "https://zip-worker.jordan1320.workers.dev/zip/a_fake_md5/null",
},
],
"average_color": null,
"download_url": "https://cdn.webampskins.org/skins/a_fake_md5.wsz",
"filename": "path.wsz",
"id": 50,
"internet_archive_item": Object {
"identifier": "a_fake_ia_identifier",
"metadata_url": "https://archive.org/metadata/a_fake_ia_identifier",
"raw_metadata_json": null,
"skin": Object {
"md5": "a_fake_md5",
},
"url": "https://archive.org/details/a_fake_ia_identifier",
},
"md5": "a_fake_md5",
"museum_url": "https://skins.webamp.org/skin/a_fake_md5",
"nsfw": false,
"readme_text": null,
"reviews": Array [],
"screenshot_url": "https://cdn.webampskins.org/screenshots/a_fake_md5.png",
"tweeted": false,
"tweets": Array [],
"webamp_url": "https://webamp.org?skinUrl=https://cdn.webampskins.org/skins/a_fake_md5.wsz",
},
}
`;

View file

@ -0,0 +1,484 @@
import { Application } from "express";
import { knex } from "../../db";
import request from "supertest"; // supertest is a framework that allows to easily test web apis
import { createApp } from "../app";
import SkinModel from "../../data/SkinModel";
import * as S3 from "../../s3";
import * as Auth from "../auth";
import { processUserUploads } from "../processUserUploads";
import UserContext from "../../data/UserContext";
import { searchIndex } from "../../algolia";
jest.mock("../../s3");
jest.mock("../../algolia");
jest.mock("../processUserUploads");
jest.mock("../auth");
let app: Application;
const handler = jest.fn();
const log = jest.fn();
const logError = jest.fn();
let username: string | undefined;
beforeEach(async () => {
jest.clearAllMocks();
username = "<MOCKED>";
app = createApp({
eventHandler: handler,
extraMiddleware: (req, res, next) => {
req.session.username = username;
next();
},
logger: { log, logError },
});
await knex.migrate.latest();
await knex.seed.run();
});
function gql(templateString: TemplateStringsArray): string {
return templateString[0];
}
async function graphQLRequest(query: string, variables?: any) {
const { body } = await request(app)
.post("/graphql")
.send({ query, variables: variables ?? {} });
if (body.errors && body.errors.length) {
console.warn(body);
}
return body;
}
describe(".me", () => {
test("logged in ", async () => {
const { data } = await graphQLRequest(gql`
query {
me {
username
}
}
`);
expect(data).toEqual({ me: { username: "<MOCKED>" } });
});
test("not logged in", async () => {
username = undefined;
const { data } = await graphQLRequest(gql`
query {
me {
username
}
}
`);
expect(data).toEqual({ me: { username: null } });
});
});
test("/auth", async () => {
const { body } = await request(app)
.get("/auth")
.expect(302)
.expect(
"Location",
"https://discord.com/api/oauth2/authorize?client_id=%3CDUMMY_DISCORD_CLIENT_ID%3E&redirect_uri=https%3A%2F%2Fapi.webampskins.org%2Fauth%2Fdiscord&response_type=code&scope=identify%20guilds"
);
expect(body).toEqual({});
});
describe("/auth/discord", () => {
test("valid code", async () => {
const response = await request(app)
.get("/auth/discord")
.query({ code: "<A_FAKE_CODE>" })
.expect(302)
.expect("Location", "https://skins.webamp.org/review/");
// TODO: Assert that we get cookie headers. I think that will not work now
// because express does not think it's secure in a test env.
expect(Auth.auth).toHaveBeenCalledWith("<A_FAKE_CODE>");
expect(response.body).toEqual({});
});
test("missing code", async () => {
const { body } = await request(app).get("/auth/discord").expect(400);
expect(Auth.auth).not.toHaveBeenCalled();
expect(body).toEqual({ message: "Expected to get a code" });
});
});
describe("Query.skins", () => {
test("no query params", async () => {
const { data } = await graphQLRequest(
gql`
query {
skins(sort: MUSEUM) {
count
nodes {
md5
filename
nsfw
}
}
}
`
);
expect(data.skins).toMatchInlineSnapshot(`
Object {
"count": 6,
"nodes": Array [
Object {
"filename": "Zelda_Amp_3.wsz",
"md5": "48bbdbbeb03d347e59b1eebda4d352d0",
"nsfw": false,
},
Object {
"filename": "path.wsz",
"md5": "a_fake_md5",
"nsfw": false,
},
Object {
"filename": "tweeted.wsz",
"md5": "a_tweeted_md5",
"nsfw": false,
},
Object {
"filename": "approved.wsz",
"md5": "an_approved_md5",
"nsfw": false,
},
Object {
"filename": "rejected.wsz",
"md5": "a_rejected_md5",
"nsfw": false,
},
Object {
"filename": "nsfw.wsz",
"md5": "a_nsfw_md5",
"nsfw": true,
},
],
}
`);
});
test("first and offset", async () => {
const { data } = await graphQLRequest(
gql`
query MyQuery($first: Int, $offset: Int) {
skins(first: $first, offset: $offset, sort: MUSEUM) {
count
nodes {
md5
filename
nsfw
}
}
}
`,
{ first: 2, offset: 1 }
);
expect(data.skins).toMatchInlineSnapshot(`
Object {
"count": 6,
"nodes": Array [
Object {
"filename": "path.wsz",
"md5": "a_fake_md5",
"nsfw": false,
},
Object {
"filename": "tweeted.wsz",
"md5": "a_tweeted_md5",
"nsfw": false,
},
],
}
`);
});
});
test("Query.fetch_skin_by_md5 (debug data)", async () => {
const { data } = await graphQLRequest(
gql`
query MyQuery($md5: String!) {
fetch_skin_by_md5(md5: $md5) {
id
md5
museum_url
webamp_url
screenshot_url
download_url
filename
readme_text
nsfw
average_color
tweeted
tweets {
url
}
archive_files {
filename
url
date
file_md5
size
text_content
is_directory
skin {
md5
}
}
filename
internet_archive_item {
identifier
url
metadata_url
raw_metadata_json
skin {
md5
}
}
reviews {
skin {
md5
}
reviewer
rating
}
}
}
`,
{ md5: "a_fake_md5" }
);
expect(data).toMatchSnapshot();
});
test("Mutation.request_nsfw_review_for_skin", async () => {
const { data } = await graphQLRequest(
gql`
mutation ($md5: String!) {
request_nsfw_review_for_skin(md5: $md5)
}
`,
{ md5: "a_fake_md5" }
);
expect(handler).toHaveBeenCalledWith({
type: "REVIEW_REQUESTED",
md5: "a_fake_md5",
});
expect(data).toEqual({ request_nsfw_review_for_skin: true });
});
test("Mutation.approve_skin", async () => {
const ctx = new UserContext();
const { data } = await graphQLRequest(
gql`
mutation ($md5: String!) {
approve_skin(md5: $md5)
}
`,
{ md5: "a_fake_md5" }
);
expect(handler).toHaveBeenCalledWith({
type: "APPROVED_SKIN",
md5: "a_fake_md5",
});
expect(data).toEqual({ approve_skin: true });
const skin = await SkinModel.fromMd5(ctx, "a_fake_md5");
expect(await skin?.getTweetStatus()).toEqual("APPROVED");
});
test("Mutation.reject_skin", async () => {
const ctx = new UserContext();
const { data } = await graphQLRequest(
gql`
mutation ($md5: String!) {
reject_skin(md5: $md5)
}
`,
{ md5: "a_fake_md5" }
);
expect(handler).toHaveBeenCalledWith({
type: "REJECTED_SKIN",
md5: "a_fake_md5",
});
expect(data).toEqual({ reject_skin: true });
const skin = await SkinModel.fromMd5(ctx, "a_fake_md5");
expect(await skin?.getTweetStatus()).toEqual("REJECTED");
});
test("Mutation.mark_skin_nsfw", async () => {
const ctx = new UserContext();
const { data } = await graphQLRequest(
gql`
mutation ($md5: String!) {
mark_skin_nsfw(md5: $md5)
}
`,
{ md5: "a_fake_md5" }
);
expect(handler).toHaveBeenCalledWith({
type: "MARKED_SKIN_NSFW",
md5: "a_fake_md5",
});
expect(searchIndex.partialUpdateObjects).toHaveBeenCalledWith([
{ nsfw: true, objectID: "a_fake_md5" },
]);
expect(data).toEqual({ mark_skin_nsfw: true });
const skin = await SkinModel.fromMd5(ctx, "a_fake_md5");
expect(await skin?.getTweetStatus()).toEqual("NSFW");
});
describe("Query.skin_to_review", () => {
test("logged in ", async () => {
const { data } = await graphQLRequest(
gql`
query {
skin_to_review {
md5
filename
}
}
`
);
expect(data).toEqual({
skin_to_review: {
filename: expect.any(String),
md5: expect.any(String),
},
});
});
test("not logged in ", async () => {
username = undefined;
const { data } = await graphQLRequest(
gql`
query {
skin_to_review {
md5
filename
}
}
`
);
expect(data).toEqual({ skin_to_review: null });
});
});
// TODO: Actually upload some skins?
test("Query.upload_statuses_by_md5", async () => {
const { data } = await graphQLRequest(
gql`
query ($md5s: [String!]!) {
upload_statuses_by_md5(md5s: $md5s) {
id
status
upload_md5
}
}
`,
{ md5s: ["a_fake_md5", "a_missing_md5"] }
);
expect(data.upload_statuses_by_md5).toEqual([]);
});
test("Mutation.upload.get_upload_urls", async () => {
const { data } = await graphQLRequest(
gql`
mutation ($files: [UploadUrlRequest!]!) {
upload {
get_upload_urls(files: $files) {
id
url
md5
}
}
}
`,
{
files: [
{
md5: "3b73bcd43c30b85d4cad3083e8ac9695",
filename: "a_fake_new_file.wsz",
},
{
md5: "48bbdbbeb03d347e59b1eebda4d352d0",
filename: "a_new_name_for_a_file_that_exists.wsz",
},
],
}
);
expect(S3.getSkinUploadUrl).toHaveBeenCalledWith(
"3b73bcd43c30b85d4cad3083e8ac9695",
expect.any(Number)
);
expect(data.upload.get_upload_urls).toEqual([
{
md5: "3b73bcd43c30b85d4cad3083e8ac9695",
id: expect.any(String),
url: "<MOCK_S3_UPLOAD_URL>",
},
]);
});
test("An Upload Flow", async () => {
// Request an upload URL
const md5 = "3b73bcd43c30b85d4cad3083e8ac9695";
const filename = "a_fake_new_file.wsz";
const { data } = await graphQLRequest(
gql`
mutation ($files: [UploadUrlRequest!]!) {
upload {
get_upload_urls(files: $files) {
id
url
md5
}
}
}
`,
{ files: [{ md5, filename }] }
);
expect(data.upload.get_upload_urls).toEqual([
{
md5,
id: expect.any(String),
url: "<MOCK_S3_UPLOAD_URL>",
},
]);
const id = data.upload.get_upload_urls[0].id;
const requestedUpload = await knex("skin_uploads").where({ id }).first();
expect(requestedUpload).toEqual({
filename,
id: Number(id),
skin_md5: md5,
status: "URL_REQUESTED",
});
// Report that we've uploaded the skin to S3 (we lie)
const { data: uploadReportData } = await graphQLRequest(
gql`
mutation ($id: String!, $md5: String!) {
upload {
report_skin_uploaded(id: $id, md5: $md5)
}
}
`,
{ md5, id }
);
expect(uploadReportData.upload.report_skin_uploaded).toEqual(true);
expect(processUserUploads).toHaveBeenCalled();
const reportedUpload = await knex("skin_uploads").where({ id }).first();
expect(reportedUpload).toEqual({
filename,
id: Number(id),
skin_md5: md5,
status: "UPLOAD_REPORTED",
});
});

View file

@ -53,7 +53,7 @@ test("/auth", async () => {
.expect(302)
.expect(
"Location",
"https://discord.com/api/oauth2/authorize?client_id=560264562222432304&redirect_uri=https%3A%2F%2Fapi.webampskins.org%2Fauth%2Fdiscord&response_type=code&scope=identify%20guilds"
"https://discord.com/api/oauth2/authorize?client_id=%3CDUMMY_DISCORD_CLIENT_ID%3E&redirect_uri=https%3A%2F%2Fapi.webampskins.org%2Fauth%2Fdiscord&response_type=code&scope=identify%20guilds"
);
expect(body).toEqual({});
});

View file

@ -84,6 +84,8 @@ export function createApp({ eventHandler, extraMiddleware, logger }: Options) {
name: "session",
secret: SECRET,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
// @ts-ignore Tests fail if this is missing, but prod is fine.
keys: "what",
})
);

View file

@ -0,0 +1,27 @@
// In the real app, these are set via .env
process.env.LOCAL_FILE_CACHE = "";
process.env.CLOUDFLARE_PURGE_AUTH_KEY = "";
process.env.CAPTBARITONE_USER_ID = "";
process.env.TEST_CHANNEL_ID = "";
process.env.TWEET_BOT_CHANNEL_ID = "";
process.env.SKIN_UPLOADS_CHANNEL_ID = "";
process.env.SKIN_REVIEW_CHANNEL_ID = "";
process.env.NSFW_SKIN_CHANNEL_ID = "";
process.env.FEEDBACK_SKIN_CHANNEL_ID = "";
process.env.POPULAR_TWEETS_CHANNEL_ID = "";
process.env.DISCORD_TOKEN = "";
process.env.DISCORD_WEBAMP_SERVER_ID = "<DUMMY_DISCORD_WEBAMP_SERVER_ID>";
process.env.DISCORD_CLIENT_ID = "<DUMMY_DISCORD_CLIENT_ID>";
process.env.DISCORD_CLIENT_SECRET = "<DUMMY_DISCORD_CLIENT_SECRET>";
process.env.DISCORD_REDIRECT_URL = "https://api.webampskins.org/auth/discord";
process.env.LOGIN_REDIRECT_URL = "https://skins.webamp.org/review/";
process.env.ALGOLIA_ACCOUNT = "";
process.env.ALGOLIA_INDEX = "";
process.env.ALGOLIA_KEY = "";
process.env.TWITTER_API_KEY = "";
process.env.TWITTER_API_SECRET = "";
process.env.TWITTER_ACCESS_TOKEN = "";
process.env.TWITTER_ACCESS_TOKEN_SECRET = "";
process.env.INSTAGRAM_ACCESS_TOKEN = "";
process.env.INSTAGRAM_ACCOUNT_ID = "";
process.env.SECRET = "";

View file

@ -1,16 +1,15 @@
import path from "path";
import { PROJECT_ROOT } from "./config";
const production = {
client: "sqlite3",
connection: {
filename: path.join(PROJECT_ROOT, "./skins.sqlite3"),
filename: path.join(__dirname, "./skins.sqlite3"),
},
useNullAsDefault: true,
debug: false,
migrations: {
tableName: "knex_migrations",
directory: path.join(PROJECT_ROOT, "./migrations"),
directory: path.join(__dirname, "./migrations"),
},
};
@ -20,14 +19,14 @@ const configs = {
connection: ":memory:",
seeds: {
// Only put this in the test config so that we ensure we never clobber prod data.
directory: path.join(PROJECT_ROOT, "./seeds"),
directory: path.join(__dirname, "./seeds"),
},
},
development: {
...production,
connection: {
...production.connection,
filename: path.join(PROJECT_ROOT, "./skins-dev.sqlite3"),
filename: path.join(__dirname, "./skins-dev.sqlite3"),
},
},
production,