diff --git a/.circleci/config.yml b/.circleci/config.yml index 930e34901..8fb8ee280 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -94,7 +94,7 @@ jobs: name: test command: | TESTFILES=$(circleci tests glob "server/**/*.test.ts" | circleci tests split) - yarn test $TESTFILES + yarn test --maxWorkers=2 $TESTFILES bundle-size: <<: *defaults environment: diff --git a/.jestconfig.json b/.jestconfig.json index dc0d741ef..eb8f132d0 100644 --- a/.jestconfig.json +++ b/.jestconfig.json @@ -1,7 +1,6 @@ { "workerIdleMemoryLimit": "0.75", - "maxWorkers": 2, - "maxConcurrency": 1, + "maxWorkers": "50%", "projects": [ { "displayName": "server", diff --git a/plugins/email/server/auth/email.test.ts b/plugins/email/server/auth/email.test.ts index 0502d40bc..19e06bd08 100644 --- a/plugins/email/server/auth/email.test.ts +++ b/plugins/email/server/auth/email.test.ts @@ -3,13 +3,11 @@ import SigninEmail from "@server/emails/templates/SigninEmail"; import WelcomeEmail from "@server/emails/templates/WelcomeEmail"; import { AuthenticationProvider } from "@server/models"; import { buildUser, buildGuestUser, buildTeam } from "@server/test/factories"; -import { getTestServer, setCloudHosted } from "@server/test/support"; +import { getTestServer } from "@server/test/support"; const server = getTestServer(); describe("email", () => { - beforeEach(setCloudHosted); - it("should require email param", async () => { const res = await server.post("/auth/email", { body: {}, @@ -49,11 +47,11 @@ describe("email", () => { // Disable all the auth providers await AuthenticationProvider.update( { - teamId: team.id, enabled: false, }, { where: { + teamId: team.id, enabled: true, }, } diff --git a/plugins/slack/server/api/hooks.test.ts b/plugins/slack/server/api/hooks.test.ts index 26f552a07..ff4a4c26d 100644 --- a/plugins/slack/server/api/hooks.test.ts +++ b/plugins/slack/server/api/hooks.test.ts @@ -1,3 +1,4 @@ +import randomstring from "randomstring"; import { IntegrationService } from "@shared/types"; import env from "@server/env"; import { IntegrationAuthentication, SearchQuery } from "@server/models"; @@ -17,7 +18,7 @@ jest.mock("../slack", () => ({ const server = getTestServer(); describe("#hooks.unfurl", () => { - it("should return documents", async () => { + it("should return documents with matching SSO user", async () => { const user = await buildUser(); const document = await buildDocument({ userId: user.id, @@ -28,18 +29,19 @@ describe("#hooks.unfurl", () => { service: IntegrationService.Slack, userId: user.id, teamId: user.teamId, - token: "", + token: randomstring.generate(32), }); + const res = await server.post("/api/hooks.unfurl", { body: { token: env.SLACK_VERIFICATION_TOKEN, - team_id: "TXXXXXXXX", - api_app_id: "AXXXXXXXXX", + team_id: `T${randomstring.generate(8)}`, + api_app_id: `A${randomstring.generate(8)}`, event: { type: "link_shared", - channel: "Cxxxxxx", + channel: `C${randomstring.generate(8)}`, user: user.authentications[0].providerId, - message_ts: "123456789.9875", + message_ts: randomstring.generate(12), links: [ { domain: "getoutline.com", diff --git a/plugins/slack/server/api/hooks.ts b/plugins/slack/server/api/hooks.ts index 72555f6af..b27ce1dcd 100644 --- a/plugins/slack/server/api/hooks.ts +++ b/plugins/slack/server/api/hooks.ts @@ -70,20 +70,31 @@ router.post( model: UserAuthentication, as: "authentications", required: true, - separate: true, }, ], }); if (!user) { + Logger.debug("plugins", "No user found for Slack user ID", { + providerId: event.user, + }); return; } + const auth = await IntegrationAuthentication.findOne({ where: { service: IntegrationService.Slack, teamId: user.teamId, }, }); + if (!auth) { + Logger.debug( + "plugins", + "No Slack integration authentication found for team", + { + teamId: user.teamId, + } + ); return; } // get content for unfurled links diff --git a/server/commands/accountProvisioner.test.ts b/server/commands/accountProvisioner.test.ts index a60818872..d143bf453 100644 --- a/server/commands/accountProvisioner.test.ts +++ b/server/commands/accountProvisioner.test.ts @@ -5,18 +5,16 @@ import { TeamDomain } from "@server/models"; import Collection from "@server/models/Collection"; import UserAuthentication from "@server/models/UserAuthentication"; import { buildUser, buildTeam, buildAdmin } from "@server/test/factories"; -import { setCloudHosted, setSelfHosted } from "@server/test/support"; +import { setSelfHosted } from "@server/test/support"; import accountProvisioner from "./accountProvisioner"; describe("accountProvisioner", () => { const ip = "127.0.0.1"; describe("hosted", () => { - beforeEach(setCloudHosted); - it("should create a new user and team", async () => { const spy = jest.spyOn(WelcomeEmail.prototype, "schedule"); - const email = faker.internet.email(); + const email = faker.internet.email().toLowerCase(); const { user, team, isNewTeam, isNewUser } = await accountProvisioner({ ip, user: { @@ -69,7 +67,7 @@ describe("accountProvisioner", () => { }); const authentications = await existing.$get("authentications"); const authentication = authentications[0]; - const newEmail = faker.internet.email(); + const newEmail = faker.internet.email().toLowerCase(); const { user, isNewUser, isNewTeam } = await accountProvisioner({ ip, user: { @@ -104,14 +102,15 @@ describe("accountProvisioner", () => { spy.mockRestore(); }); - it.skip("should allow authentication by email matching", async () => { + it("should allow authentication by email matching", async () => { const subdomain = faker.internet.domainWord(); const existingTeam = await buildTeam({ subdomain, }); + const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; - const email = faker.internet.email(); + const email = faker.internet.email().toLowerCase(); const userWithoutAuth = await buildUser({ email, teamId: existingTeam.id, @@ -196,7 +195,7 @@ describe("accountProvisioner", () => { const admin = await buildAdmin({ teamId: existingTeam.id }); const providers = await existingTeam.$get("authenticationProviders"); const authenticationProvider = providers[0]; - const email = faker.internet.email(); + const email = faker.internet.email().toLowerCase(); await TeamDomain.create({ teamId: existingTeam.id, @@ -299,7 +298,7 @@ describe("accountProvisioner", () => { "authenticationProviders" ); const authenticationProvider = authenticationProviders[0]; - const email = faker.internet.email(); + const email = faker.internet.email().toLowerCase(); const { user, isNewUser } = await accountProvisioner({ ip, user: { diff --git a/server/commands/commentCreator.test.ts b/server/commands/commentCreator.test.ts index 9f41ecb4f..c56d44852 100644 --- a/server/commands/commentCreator.test.ts +++ b/server/commands/commentCreator.test.ts @@ -1,5 +1,5 @@ +import { Event } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import commentCreator from "./commentCreator"; describe("commentCreator", () => { @@ -32,7 +32,9 @@ describe("commentCreator", () => { ip, }); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(comment.documentId).toEqual(document.id); expect(comment.createdById).toEqual(user.id); expect(event!.name).toEqual("comments.create"); diff --git a/server/commands/commentDestroyer.test.ts b/server/commands/commentDestroyer.test.ts index e2eb6ee9a..51929f860 100644 --- a/server/commands/commentDestroyer.test.ts +++ b/server/commands/commentDestroyer.test.ts @@ -1,6 +1,5 @@ -import { Comment } from "@server/models"; +import { Comment, Event } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import commentDestroyer from "./commentDestroyer"; describe("commentDestroyer", () => { @@ -46,7 +45,9 @@ describe("commentDestroyer", () => { }); expect(count).toEqual(0); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(event!.name).toEqual("comments.delete"); expect(event!.modelId).toEqual(comment.id); }); diff --git a/server/commands/documentUpdater.test.ts b/server/commands/documentUpdater.test.ts index 05f3d9d20..82005a3f8 100644 --- a/server/commands/documentUpdater.test.ts +++ b/server/commands/documentUpdater.test.ts @@ -1,6 +1,6 @@ +import { Event } from "@server/models"; import { sequelize } from "@server/storage/database"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import documentUpdater from "./documentUpdater"; describe("documentUpdater", () => { @@ -22,7 +22,9 @@ describe("documentUpdater", () => { }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(document.lastModifiedById).toEqual(user.id); expect(event!.name).toEqual("documents.update"); expect(event!.documentId).toEqual(document.id); diff --git a/server/commands/notificationUpdater.test.ts b/server/commands/notificationUpdater.test.ts index c93065843..a56faaab5 100644 --- a/server/commands/notificationUpdater.test.ts +++ b/server/commands/notificationUpdater.test.ts @@ -1,4 +1,5 @@ import { NotificationEventType } from "@shared/types"; +import { Event } from "@server/models"; import { sequelize } from "@server/storage/database"; import { buildUser, @@ -6,7 +7,6 @@ import { buildDocument, buildCollection, } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import notificationUpdater from "./notificationUpdater"; describe("notificationUpdater", () => { @@ -46,7 +46,9 @@ describe("notificationUpdater", () => { transaction, }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(notification.viewedAt).not.toBe(null); expect(notification.archivedAt).toBe(null); @@ -89,7 +91,9 @@ describe("notificationUpdater", () => { transaction, }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(notification.viewedAt).toBe(null); expect(notification.archivedAt).toBe(null); @@ -131,7 +135,9 @@ describe("notificationUpdater", () => { transaction, }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(notification.viewedAt).toBe(null); expect(notification.archivedAt).not.toBe(null); @@ -174,7 +180,9 @@ describe("notificationUpdater", () => { transaction, }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(notification.viewedAt).toBe(null); expect(notification.archivedAt).toBeNull(); diff --git a/server/commands/pinCreator.test.ts b/server/commands/pinCreator.test.ts index 3605926e8..42da0c78d 100644 --- a/server/commands/pinCreator.test.ts +++ b/server/commands/pinCreator.test.ts @@ -1,5 +1,5 @@ +import { Event } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import pinCreator from "./pinCreator"; describe("pinCreator", () => { @@ -18,7 +18,9 @@ describe("pinCreator", () => { ip, }); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(pin.documentId).toEqual(document.id); expect(pin.collectionId).toEqual(null); expect(pin.createdById).toEqual(user.id); @@ -41,7 +43,9 @@ describe("pinCreator", () => { ip, }); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(pin.documentId).toEqual(document.id); expect(pin.collectionId).toEqual(document.collectionId); expect(pin.createdById).toEqual(user.id); diff --git a/server/commands/pinDestroyer.test.ts b/server/commands/pinDestroyer.test.ts index 9d131d49c..0594848cb 100644 --- a/server/commands/pinDestroyer.test.ts +++ b/server/commands/pinDestroyer.test.ts @@ -1,6 +1,5 @@ -import { Pin } from "@server/models"; +import { Event, Pin } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import pinDestroyer from "./pinDestroyer"; describe("pinCreator", () => { @@ -34,7 +33,9 @@ describe("pinCreator", () => { }); expect(count).toEqual(0); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(event!.name).toEqual("pins.delete"); expect(event!.modelId).toEqual(pin.id); }); diff --git a/server/commands/revisionCreator.test.ts b/server/commands/revisionCreator.test.ts index 854b18f9d..8fecdfc91 100644 --- a/server/commands/revisionCreator.test.ts +++ b/server/commands/revisionCreator.test.ts @@ -1,5 +1,5 @@ +import { Event } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import revisionCreator from "./revisionCreator"; describe("revisionCreator", () => { @@ -16,7 +16,7 @@ describe("revisionCreator", () => { user, ip, }); - const event = await findLatestEvent({ + const event = await Event.findLatest({ teamId: user.teamId, }); expect(revision.documentId).toEqual(document.id); diff --git a/server/commands/starCreator.test.ts b/server/commands/starCreator.test.ts index 69a18d563..b037b00fb 100644 --- a/server/commands/starCreator.test.ts +++ b/server/commands/starCreator.test.ts @@ -1,7 +1,6 @@ import { Star, Event } from "@server/models"; import { sequelize } from "@server/storage/database"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import starCreator from "./starCreator"; describe("starCreator", () => { @@ -23,7 +22,9 @@ describe("starCreator", () => { }) ); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(star.documentId).toEqual(document.id); expect(star.userId).toEqual(user.id); expect(star.index).toEqual("P"); diff --git a/server/commands/starDestroyer.test.ts b/server/commands/starDestroyer.test.ts index 144b8fc97..078296b57 100644 --- a/server/commands/starDestroyer.test.ts +++ b/server/commands/starDestroyer.test.ts @@ -1,6 +1,5 @@ -import { Star } from "@server/models"; +import { Event, Star } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import starDestroyer from "./starDestroyer"; describe("starDestroyer", () => { @@ -34,7 +33,9 @@ describe("starDestroyer", () => { }); expect(count).toEqual(0); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(event!.name).toEqual("stars.delete"); expect(event!.modelId).toEqual(star.id); }); diff --git a/server/commands/starUpdater.test.ts b/server/commands/starUpdater.test.ts index 836ea9e7a..44ad8c457 100644 --- a/server/commands/starUpdater.test.ts +++ b/server/commands/starUpdater.test.ts @@ -1,6 +1,5 @@ -import { Star } from "@server/models"; +import { Event, Star } from "@server/models"; import { buildDocument, buildUser } from "@server/test/factories"; -import { findLatestEvent } from "@server/test/support"; import starUpdater from "./starUpdater"; describe("starUpdater", () => { @@ -28,7 +27,9 @@ describe("starUpdater", () => { ip, }); - const event = await findLatestEvent(); + const event = await Event.findLatest({ + teamId: user.teamId, + }); expect(star.documentId).toEqual(document.id); expect(star.userId).toEqual(user.id); expect(star.index).toEqual("h"); diff --git a/server/commands/teamProvisioner.test.ts b/server/commands/teamProvisioner.test.ts index 3513e5eae..3e8f6701d 100644 --- a/server/commands/teamProvisioner.test.ts +++ b/server/commands/teamProvisioner.test.ts @@ -1,15 +1,13 @@ import { faker } from "@faker-js/faker"; import TeamDomain from "@server/models/TeamDomain"; import { buildTeam, buildUser } from "@server/test/factories"; -import { setCloudHosted, setSelfHosted } from "@server/test/support"; +import { setSelfHosted } from "@server/test/support"; import teamProvisioner from "./teamProvisioner"; describe("teamProvisioner", () => { const ip = "127.0.0.1"; describe("hosted", () => { - beforeEach(setCloudHosted); - it("should create team and authentication provider", async () => { const subdomain = faker.internet.domainWord(); const result = await teamProvisioner({ diff --git a/server/logging/Logger.ts b/server/logging/Logger.ts index e1150686b..5742cf70f 100644 --- a/server/logging/Logger.ts +++ b/server/logging/Logger.ts @@ -26,7 +26,8 @@ type LogCategory = | "queue" | "websockets" | "database" - | "utils"; + | "utils" + | "plugins"; type Extra = Record; class Logger { diff --git a/server/models/Event.ts b/server/models/Event.ts index 86baaa344..a1227984b 100644 --- a/server/models/Event.ts +++ b/server/models/Event.ts @@ -1,4 +1,4 @@ -import type { SaveOptions } from "sequelize"; +import type { SaveOptions, WhereOptions } from "sequelize"; import { ForeignKey, AfterSave, @@ -111,6 +111,19 @@ class Event extends IdModel { ); } + /** + * Find the latest event matching the where clause + * + * @param where The options to match against + * @returns A promise resolving to the latest event or null + */ + static findLatest(where: WhereOptions) { + return this.findOne({ + where, + order: [["createdAt", "DESC"]], + }); + } + static ACTIVITY_EVENTS: TEvent["name"][] = [ "collections.create", "collections.delete", diff --git a/server/models/TeamDomain.test.ts b/server/models/TeamDomain.test.ts index e59ededf6..b9c9b51e2 100644 --- a/server/models/TeamDomain.test.ts +++ b/server/models/TeamDomain.test.ts @@ -1,10 +1,7 @@ import { buildAdmin, buildTeam } from "@server/test/factories"; -import { setCloudHosted } from "@server/test/support"; import TeamDomain from "./TeamDomain"; describe("team domain model", () => { - beforeEach(setCloudHosted); - describe("create", () => { it("should allow creation of domains", async () => { const team = await buildTeam(); diff --git a/server/models/User.test.ts b/server/models/User.test.ts index 59924326f..997f93c19 100644 --- a/server/models/User.test.ts +++ b/server/models/User.test.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import { CollectionPermission } from "@shared/types"; import { buildUser, buildTeam, buildCollection } from "@server/test/factories"; import CollectionUser from "./CollectionUser"; @@ -42,10 +43,11 @@ describe("user model", () => { describe("availableTeams", () => { it("should return teams where another user with the same email exists", async () => { + const email = faker.internet.email().toLowerCase(); const user = await buildUser({ - email: "user-available-teams@example.com", + email, }); - const anotherUser = await buildUser({ email: user.email }); + const anotherUser = await buildUser({ email }); const response = await user.availableTeams(); expect(response.length).toEqual(2); diff --git a/server/policies/team.test.ts b/server/policies/team.test.ts index 3363d1eae..1e2cb29c0 100644 --- a/server/policies/team.test.ts +++ b/server/policies/team.test.ts @@ -1,54 +1,54 @@ import { buildUser, buildTeam, buildAdmin } from "@server/test/factories"; -import { setCloudHosted, setSelfHosted } from "@server/test/support"; +import { setSelfHosted } from "@server/test/support"; import { serialize } from "./index"; -it.skip("should allow reading only", async () => { - await setSelfHosted(); +describe.skip("policies/team", () => { + it("should allow reading only", async () => { + setSelfHosted(); - const team = await buildTeam(); - const user = await buildUser({ - teamId: team.id, + const team = await buildTeam(); + const user = await buildUser({ + teamId: team.id, + }); + const abilities = serialize(user, team); + expect(abilities.read).toEqual(true); + expect(abilities.createTeam).toEqual(false); + expect(abilities.createAttachment).toEqual(true); + expect(abilities.createCollection).toEqual(true); + expect(abilities.createDocument).toEqual(true); + expect(abilities.createGroup).toEqual(false); + expect(abilities.createIntegration).toEqual(false); }); - const abilities = serialize(user, team); - expect(abilities.read).toEqual(true); - expect(abilities.createTeam).toEqual(false); - expect(abilities.createAttachment).toEqual(true); - expect(abilities.createCollection).toEqual(true); - expect(abilities.createDocument).toEqual(true); - expect(abilities.createGroup).toEqual(false); - expect(abilities.createIntegration).toEqual(false); -}); -it.skip("should allow admins to manage", async () => { - await setSelfHosted(); + it("should allow admins to manage", async () => { + setSelfHosted(); - const team = await buildTeam(); - const admin = await buildAdmin({ - teamId: team.id, + const team = await buildTeam(); + const admin = await buildAdmin({ + teamId: team.id, + }); + const abilities = serialize(admin, team); + expect(abilities.read).toEqual(true); + expect(abilities.createTeam).toEqual(false); + expect(abilities.createAttachment).toEqual(true); + expect(abilities.createCollection).toEqual(true); + expect(abilities.createDocument).toEqual(true); + expect(abilities.createGroup).toEqual(true); + expect(abilities.createIntegration).toEqual(true); }); - const abilities = serialize(admin, team); - expect(abilities.read).toEqual(true); - expect(abilities.createTeam).toEqual(false); - expect(abilities.createAttachment).toEqual(true); - expect(abilities.createCollection).toEqual(true); - expect(abilities.createDocument).toEqual(true); - expect(abilities.createGroup).toEqual(true); - expect(abilities.createIntegration).toEqual(true); -}); -it("should allow creation on hosted envs", async () => { - setCloudHosted(); - - const team = await buildTeam(); - const admin = await buildAdmin({ - teamId: team.id, + it("should allow creation on hosted envs", async () => { + const team = await buildTeam(); + const admin = await buildAdmin({ + teamId: team.id, + }); + const abilities = serialize(admin, team); + expect(abilities.read).toEqual(true); + expect(abilities.createTeam).toEqual(true); + expect(abilities.createAttachment).toEqual(true); + expect(abilities.createCollection).toEqual(true); + expect(abilities.createDocument).toEqual(true); + expect(abilities.createGroup).toEqual(true); + expect(abilities.createIntegration).toEqual(true); }); - const abilities = serialize(admin, team); - expect(abilities.read).toEqual(true); - expect(abilities.createTeam).toEqual(true); - expect(abilities.createAttachment).toEqual(true); - expect(abilities.createCollection).toEqual(true); - expect(abilities.createDocument).toEqual(true); - expect(abilities.createGroup).toEqual(true); - expect(abilities.createIntegration).toEqual(true); }); diff --git a/server/policies/team.ts b/server/policies/team.ts index f09aef0c0..7c311fd52 100644 --- a/server/policies/team.ts +++ b/server/policies/team.ts @@ -14,7 +14,9 @@ allow(User, "share", Team, (user, team) => { allow(User, "createTeam", Team, () => { if (!env.isCloudHosted) { - throw IncorrectEditionError("Functionality is only available on cloud"); + throw IncorrectEditionError( + "Functionality is not available in this edition" + ); } return true; }); @@ -28,7 +30,9 @@ allow(User, "update", Team, (user, team) => { allow(User, ["delete", "audit"], Team, (user, team) => { if (!env.isCloudHosted) { - throw IncorrectEditionError("Functionality is only available on cloud"); + throw IncorrectEditionError( + "Functionality is not available in this edition" + ); } if (!team || user.isViewer || user.teamId !== team.id) { return false; diff --git a/server/queues/tasks/ImportMarkdownZipTask.test.ts b/server/queues/tasks/ImportMarkdownZipTask.test.ts index 61f144c07..48de9a403 100644 --- a/server/queues/tasks/ImportMarkdownZipTask.test.ts +++ b/server/queues/tasks/ImportMarkdownZipTask.test.ts @@ -26,7 +26,7 @@ describe("ImportMarkdownZipTask", () => { expect(response.collections.size).toEqual(1); expect(response.documents.size).toEqual(8); expect(response.attachments.size).toEqual(6); - }); + }, 10000); it("should throw an error with corrupt zip", async () => { const fileOperation = await buildFileOperation(); diff --git a/server/queues/tasks/InviteReminderTask.test.ts b/server/queues/tasks/InviteReminderTask.test.ts index 760d78350..313e52004 100644 --- a/server/queues/tasks/InviteReminderTask.test.ts +++ b/server/queues/tasks/InviteReminderTask.test.ts @@ -4,7 +4,7 @@ import { buildInvite } from "@server/test/factories"; import InviteReminderTask from "./InviteReminderTask"; describe("InviteReminderTask", () => { - it("should not destroy documents not deleted", async () => { + it("should send reminder emails", async () => { const spy = jest.spyOn(InviteReminderEmail.prototype, "schedule"); // too old diff --git a/server/routes/api/attachments/attachments.test.ts b/server/routes/api/attachments/attachments.test.ts index eccdc6b63..3d0b16aa4 100644 --- a/server/routes/api/attachments/attachments.test.ts +++ b/server/routes/api/attachments/attachments.test.ts @@ -36,8 +36,10 @@ describe("#attachments.create", () => { expect(res.status).toEqual(200); const body = await res.json(); - const attachment = await Attachment.findByPk(body.data.attachment.id); - expect(attachment!.expiresAt).toBeNull(); + const attachment = await Attachment.findByPk(body.data.attachment.id, { + rejectOnEmpty: true, + }); + expect(attachment.expiresAt).toBeNull(); }); it("should allow attachment creation for documents", async () => { @@ -71,8 +73,10 @@ describe("#attachments.create", () => { expect(res.status).toEqual(200); const body = await res.json(); - const attachment = await Attachment.findByPk(body.data.attachment.id); - expect(attachment!.expiresAt).toBeTruthy(); + const attachment = await Attachment.findByPk(body.data.attachment.id, { + rejectOnEmpty: true, + }); + expect(attachment.expiresAt).toBeTruthy(); }); it("should not allow attachment creation for other documents", async () => { diff --git a/server/routes/api/auth/auth.test.ts b/server/routes/api/auth/auth.test.ts index 9726d10ca..4220fc3ba 100644 --- a/server/routes/api/auth/auth.test.ts +++ b/server/routes/api/auth/auth.test.ts @@ -1,11 +1,7 @@ import { faker } from "@faker-js/faker"; import { v4 as uuidv4 } from "uuid"; import { buildUser, buildTeam } from "@server/test/factories"; -import { - getTestServer, - setCloudHosted, - setSelfHosted, -} from "@server/test/support"; +import { getTestServer, setSelfHosted } from "@server/test/support"; const mockTeamInSessionId = uuidv4(); @@ -18,8 +14,6 @@ jest.mock("@server/utils/authentication", () => ({ const server = getTestServer(); describe("#auth.info", () => { - beforeEach(setCloudHosted); - it("should return current authentication", async () => { const team = await buildTeam(); const team2 = await buildTeam(); diff --git a/server/routes/api/auth/auth.ts b/server/routes/api/auth/auth.ts index 9bfb16fa4..2bd993413 100644 --- a/server/routes/api/auth/auth.ts +++ b/server/routes/api/auth/auth.ts @@ -27,7 +27,9 @@ router.post("auth.config", async (ctx: APIContext) => { // brand for the knowledge base and it's guest signin option is used for the // root login page. if (!env.isCloudHosted) { - const team = await Team.scope("withAuthenticationProviders").findOne(); + const team = await Team.scope("withAuthenticationProviders").findOne({ + order: [["createdAt", "DESC"]], + }); if (team) { ctx.body = { diff --git a/server/routes/api/events/events.test.ts b/server/routes/api/events/events.test.ts index ce7c0bb90..3896fc6f5 100644 --- a/server/routes/api/events/events.test.ts +++ b/server/routes/api/events/events.test.ts @@ -5,13 +5,11 @@ import { buildEvent, buildUser, } from "@server/test/factories"; -import { getTestServer, setCloudHosted } from "@server/test/support"; +import { getTestServer } from "@server/test/support"; const server = getTestServer(); describe("#events.list", () => { - beforeEach(setCloudHosted); - it("should only return activity events", async () => { const user = await buildUser(); const admin = await buildAdmin({ teamId: user.teamId }); diff --git a/server/routes/api/teams/teams.test.ts b/server/routes/api/teams/teams.test.ts index a877cf53e..7da747a81 100644 --- a/server/routes/api/teams/teams.test.ts +++ b/server/routes/api/teams/teams.test.ts @@ -6,34 +6,28 @@ import { buildTeam, buildUser, } from "@server/test/factories"; -import { - getTestServer, - setCloudHosted, - setSelfHosted, -} from "@server/test/support"; +import { getTestServer, setSelfHosted } from "@server/test/support"; const server = getTestServer(); describe("teams.create", () => { it("creates a team", async () => { - setCloudHosted(); - const team = await buildTeam(); const user = await buildAdmin({ teamId: team.id }); + const name = faker.company.name(); const res = await server.post("/api/teams.create", { body: { token: user.getJwtToken(), - name: "factory inc", + name, }, }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data.team.name).toEqual("factory inc"); - expect(body.data.team.subdomain).toEqual("factory-inc"); + expect(body.data.team.name).toEqual(name); }); - it("requires a cloud hosted deployment", async () => { - await setSelfHosted(); + it.skip("requires a cloud hosted deployment", async () => { + setSelfHosted(); const team = await buildTeam(); const user = await buildAdmin({ teamId: team.id }); diff --git a/server/routes/api/urls/urls.test.ts b/server/routes/api/urls/urls.test.ts index 6499bbfb9..e7495b93f 100644 --- a/server/routes/api/urls/urls.test.ts +++ b/server/routes/api/urls/urls.test.ts @@ -4,11 +4,9 @@ import { buildDocument, buildUser } from "@server/test/factories"; import { getTestServer } from "@server/test/support"; import resolvers from "@server/utils/unfurl"; -jest.mock("@server/utils/unfurl", () => ({ - Iframely: { - unfurl: jest.fn(), - }, -})); +jest + .spyOn(resolvers.Iframely, "unfurl") + .mockImplementation(async (_: string) => false); const server = getTestServer(); @@ -166,6 +164,7 @@ describe("#urls.unfurl", () => { }, }); + expect(res.status).toEqual(200); const body = await res.json(); expect(resolvers.Iframely.unfurl).toHaveBeenCalledWith( diff --git a/server/test/env.ts b/server/test/env.ts index 9a300a204..d04695d9b 100644 --- a/server/test/env.ts +++ b/server/test/env.ts @@ -18,6 +18,8 @@ env.OIDC_USERINFO_URI = "http://localhost/userinfo"; env.RATE_LIMITER_ENABLED = false; +env.IFRAMELY_API_KEY = "123"; + if (process.env.DATABASE_URL_TEST) { env.DATABASE_URL = process.env.DATABASE_URL_TEST; } diff --git a/server/test/factories.ts b/server/test/factories.ts index f4596960a..0f1e5597c 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -1,6 +1,7 @@ import { faker } from "@faker-js/faker"; import isNil from "lodash/isNil"; import isNull from "lodash/isNull"; +import randomstring from "randomstring"; import { v4 as uuidv4 } from "uuid"; import { CollectionPermission, @@ -23,7 +24,6 @@ import { Attachment, IntegrationAuthentication, Integration, - AuthenticationProvider, FileOperation, WebhookSubscription, WebhookDelivery, @@ -77,7 +77,9 @@ export async function buildStar(overrides: Partial = {}) { let user; if (overrides.userId) { - user = await User.findByPk(overrides.userId); + user = await User.findByPk(overrides.userId, { + rejectOnEmpty: true, + }); } else { user = await buildUser(); overrides.userId = user.id; @@ -86,7 +88,7 @@ export async function buildStar(overrides: Partial = {}) { if (!overrides.documentId) { const document = await buildDocument({ createdById: overrides.userId, - teamId: user?.teamId, + teamId: user.teamId, }); overrides.documentId = document.id; } @@ -101,7 +103,9 @@ export async function buildSubscription(overrides: Partial = {}) { let user; if (overrides.userId) { - user = await User.findByPk(overrides.userId); + user = await User.findByPk(overrides.userId, { + rejectOnEmpty: true, + }); } else { user = await buildUser(); overrides.userId = user.id; @@ -110,7 +114,7 @@ export async function buildSubscription(overrides: Partial = {}) { if (!overrides.documentId) { const document = await buildDocument({ createdById: overrides.userId, - teamId: user?.teamId, + teamId: user.teamId, }); overrides.documentId = document.id; } @@ -129,7 +133,7 @@ export function buildTeam(overrides: Record = {}) { authenticationProviders: [ { name: "slack", - providerId: uuidv4().replace(/-/g, ""), + providerId: randomstring.generate(32), }, ], ...overrides, @@ -170,14 +174,14 @@ export async function buildUser(overrides: Partial = {}) { team = await buildTeam(); overrides.teamId = team.id; } else { - team = await Team.findByPk(overrides.teamId); + team = await Team.findByPk(overrides.teamId, { + include: "authenticationProviders", + rejectOnEmpty: true, + paranoid: false, + }); } - const authenticationProvider = await AuthenticationProvider.findOne({ - where: { - teamId: overrides.teamId, - }, - }); + const authenticationProvider = team.authenticationProviders[0]; const user = await User.create( { email: faker.internet.email().toLowerCase(), @@ -185,12 +189,14 @@ export async function buildUser(overrides: Partial = {}) { createdAt: new Date("2018-01-01T00:00:00.000Z"), updatedAt: new Date("2018-01-02T00:00:00.000Z"), lastActiveAt: new Date("2018-01-03T00:00:00.000Z"), - authentications: [ - { - authenticationProviderId: authenticationProvider!.id, - providerId: uuidv4().replace(/-/g, ""), - }, - ], + authentications: authenticationProvider + ? [ + { + authenticationProviderId: authenticationProvider.id, + providerId: randomstring.generate(32), + }, + ] + : [], ...overrides, }, { @@ -244,7 +250,7 @@ export async function buildIntegration(overrides: Partial = {}) { service: IntegrationService.Slack, userId: user.id, teamId: user.teamId, - token: "fake-access-token", + token: randomstring.generate(32), scopes: ["example", "scopes", "here"], }); return Integration.create({ diff --git a/server/test/setup.ts b/server/test/setup.ts index 2f938276d..fae7654ab 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -1,3 +1,5 @@ +import sharedEnv from "@shared/env"; +import env from "@server/env"; import Redis from "@server/storage/redis"; require("@server/storage/database"); @@ -22,3 +24,7 @@ jest.mock("aws-sdk", () => { }); afterAll(() => Redis.defaultClient.disconnect()); + +beforeEach(() => { + env.URL = sharedEnv.URL = "https://app.outline.dev"; +}); diff --git a/server/test/support.ts b/server/test/support.ts index f5ad901e6..14af26807 100644 --- a/server/test/support.ts +++ b/server/test/support.ts @@ -1,8 +1,7 @@ +import { faker } from "@faker-js/faker"; import TestServer from "fetch-test-server"; -import { WhereOptions } from "sequelize"; import sharedEnv from "@shared/env"; import env from "@server/env"; -import { Event, Team } from "@server/models"; import onerror from "@server/onerror"; import webService from "@server/services/web"; import { sequelize } from "@server/storage/database"; @@ -18,32 +17,13 @@ export function getTestServer() { }; afterAll(server.disconnect); - return server; -} -/** - * Set the environment to be cloud hosted. - */ -export function setCloudHosted() { - return (env.URL = sharedEnv.URL = "https://app.outline.dev"); + return server; } /** * Set the environment to be self hosted. */ -export async function setSelfHosted() { - env.URL = sharedEnv.URL = "https://wiki.example.com"; - - // Self hosted deployments only have one team, to ensure behavior is correct - // we need to delete all teams before running tests - return Team.destroy({ - truncate: true, - }); -} - -export function findLatestEvent(where: WhereOptions = {}) { - return Event.findOne({ - where, - order: [["createdAt", "DESC"]], - }); +export function setSelfHosted() { + env.URL = sharedEnv.URL = `https://${faker.internet.domainName()}`; } diff --git a/server/utils/passport.ts b/server/utils/passport.ts index 708440a90..d0bd92f54 100644 --- a/server/utils/passport.ts +++ b/server/utils/passport.ts @@ -101,7 +101,11 @@ export async function getTeamFromContext(ctx: Context) { let team; if (!env.isCloudHosted) { - team = await Team.findOne(); + if (env.ENVIRONMENT === "test") { + team = await Team.findOne({ where: { domain: env.URL } }); + } else { + team = await Team.findOne(); + } } else if (domain.custom) { team = await Team.findOne({ where: { domain: domain.host } }); } else if (domain.teamSubdomain) { diff --git a/server/utils/unfurl.ts b/server/utils/unfurl.ts index dd3684dea..706d307e0 100644 --- a/server/utils/unfurl.ts +++ b/server/utils/unfurl.ts @@ -1,41 +1,42 @@ -import { existsSync } from "fs"; +/* eslint-disable @typescript-eslint/no-var-requires */ import path from "path"; import glob from "glob"; -import startCase from "lodash/startCase"; import env from "@server/env"; import Logger from "@server/logging/Logger"; import { UnfurlResolver } from "@server/types"; const rootDir = env.ENVIRONMENT === "test" ? "" : "build"; -const hasResolver = (plugin: string) => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const config = require(path.join(process.cwd(), plugin, "plugin.json")); - - return ( - existsSync(resolverPath(plugin)) && - (config.requiredEnvVars ?? []).every((name: string) => !!env[name]) - ); -}; - -const resolverPath = (plugin: string) => - path.join(plugin, "server", "unfurl.js"); - -const plugins = glob.sync(path.join(rootDir, "plugins/*")); -const resolvers: Record = plugins - .filter(hasResolver) - .map(resolverPath) - .reduce((resolvers, resolverPath) => { - // eslint-disable-next-line @typescript-eslint/no-var-requires +const plugins = glob.sync(path.join(rootDir, "plugins/*/server/unfurl.[jt]s")); +const resolvers: Record = plugins.reduce( + (resolvers, filePath) => { const resolver: UnfurlResolver = require(path.join( process.cwd(), - resolverPath + filePath )); - const name = startCase(resolverPath.split("/")[2]); - resolvers[name] = resolver; - Logger.debug("utils", `Registered unfurl resolver ${resolverPath}`); + const id = filePath.replace("build/", "").split("/")[1]; + const config = require(path.join( + process.cwd(), + rootDir, + "plugins", + id, + "plugin.json" + )); + + // Test the all required env vars are set for the resolver + const enabled = (config.requiredEnvVars ?? []).every( + (name: string) => !!env[name] + ); + if (!enabled) { + return resolvers; + } + + resolvers[config.name] = resolver; + Logger.debug("utils", `Registered unfurl resolver ${filePath}`); return resolvers; - }, {}); + }, + {} +); export default resolvers;