Skip to content

Commit

Permalink
refactor: extract force join organization
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasduteil committed Feb 19, 2025
1 parent 0b1017d commit c6a6098
Show file tree
Hide file tree
Showing 24 changed files with 289 additions and 61 deletions.
18 changes: 15 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/identite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@gouvfr-lasuite/proconnect.core": "^0.4.0",
"@gouvfr-lasuite/proconnect.devtools.typescript": "0.0.0",
"@gouvfr-lasuite/proconnect.insee": "^0.3.2",
"@sinonjs/fake-timers": "^14.0.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.10.2",
"await-to-js": "^3.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/identite/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class NotFoundError extends Error {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NotFoundError } from "#src/errors";
import type { Organization, User } from "#src/types";
import * as chai from "chai";
import "chai-as-promised";
import chaiAsPromised from "chai-as-promised";
import { describe } from "mocha";
import { forceJoinOrganizationFactory } from "./force-join-organization.js";
chai.use(chaiAsPromised);
const expect = chai.expect;

describe(forceJoinOrganizationFactory.name, () => {
it("should update the organization user link ", async () => {
const forceJoinOrganization = forceJoinOrganizationFactory({
findById: () => Promise.resolve({ id: 42 } as Organization),
findEmailDomainsByOrganizationId: () => Promise.resolve([]),
findUserById: () =>
Promise.resolve({ email: "[email protected]" } as User),
linkUserToOrganization: (values) => Promise.resolve(values as any),
});

await expect(
forceJoinOrganization({
organization_id: 42,
user_id: 42,
}),
).eventually.deep.equal({
is_external: false,
organization_id: 42,
user_id: 42,
verification_type: "no_validation_means_available",
});
});

it("❎ throws NotFoundError for unknown organization", async () => {
const forceJoinOrganization = forceJoinOrganizationFactory({
findById: () => Promise.resolve(undefined),
findEmailDomainsByOrganizationId: () => Promise.resolve([]),
findUserById: () => Promise.resolve({ id: 42 } as User),
linkUserToOrganization: () => Promise.reject(),
});

await expect(
forceJoinOrganization({
organization_id: 42,
user_id: 42,
}),
).rejectedWith(NotFoundError);
});

it("❎ throws NotFoundError for unknown user", async () => {
const forceJoinOrganization = forceJoinOrganizationFactory({
findById: () => Promise.resolve({ id: 42 } as Organization),
findEmailDomainsByOrganizationId: () => Promise.resolve([]),
findUserById: () => Promise.resolve(undefined),
linkUserToOrganization: () => Promise.reject(),
});

await expect(
forceJoinOrganization({
organization_id: 42,
user_id: 42,
}),
).rejectedWith(NotFoundError);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//

import { NotFoundError } from "#src/errors";
import type { FindEmailDomainsByOrganizationIdHandler } from "#src/repositories/email-domain";
import type {
FindByIdHandler as FindOrganizationByIdHandler,
LinkUserToOrganizationHandler,
} from "#src/repositories/organization";
import type { FindByIdHandler as FindUserByIdHandler } from "#src/repositories/user";
import type { BaseUserOrganizationLink } from "#src/types";
import { getEmailDomain } from "@gouvfr-lasuite/proconnect.core/services/email";
import { isEmpty, some } from "lodash-es";

//

type FactoryDependencies = {
findById: FindOrganizationByIdHandler;
findEmailDomainsByOrganizationId: FindEmailDomainsByOrganizationIdHandler;
findUserById: FindUserByIdHandler;
linkUserToOrganization: LinkUserToOrganizationHandler;
};

//

export function forceJoinOrganizationFactory({
findById,
findEmailDomainsByOrganizationId,
findUserById,
linkUserToOrganization,
}: FactoryDependencies) {
return async function forceJoinOrganization({
organization_id,
user_id,
is_external = false,
}: {
organization_id: number;
user_id: number;
is_external?: boolean;
}) {
const user = await findUserById(user_id);
const organization = await findById(organization_id);
if (isEmpty(user) || isEmpty(organization)) {
throw new NotFoundError();
}
const { email } = user;
const domain = getEmailDomain(email);
const organizationEmailDomains =
await findEmailDomainsByOrganizationId(organization_id);

let link_verification_type: BaseUserOrganizationLink["verification_type"];
if (
some(organizationEmailDomains, {
domain,
verification_type: "verified",
}) ||
some(organizationEmailDomains, {
domain,
verification_type: "trackdechets_postal_mail",
}) ||
some(organizationEmailDomains, { domain, verification_type: "external" })
) {
link_verification_type = "domain";
} else {
link_verification_type = "no_validation_means_available";
}

return await linkUserToOrganization({
organization_id,
user_id,
is_external,
verification_type: link_verification_type,
});
};
}
1 change: 1 addition & 0 deletions packages/identite/src/managers/organization/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//

export * from "./force-join-organization.js";
export * from "./get-organization-info.js";
export * from "./mark-domain-as-verified.js";
1 change: 1 addition & 0 deletions packages/identite/src/repositories/organization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

export * from "./find-by-id.js";
export * from "./get-users-by-organization.js";
export * from "./link-user-to-organization.js";
export * from "./upsert.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { emptyDatabase, migrate, pg } from "#testing";
import FakeTimers from "@sinonjs/fake-timers";
import * as chai from "chai";
import "chai-as-promised";
import chaiAsPromised from "chai-as-promised";
import { describe } from "mocha";
import { linkUserToOrganizationFactory } from "./link-user-to-organization.js";
chai.use(chaiAsPromised);
const expect = chai.expect;

//

const linkUserToOrganization = linkUserToOrganizationFactory({ pg: pg as any });

describe(linkUserToOrganizationFactory.name, () => {
before(migrate);
beforeEach(emptyDatabase);

it("should link user to organization", async () => {
await pg.sql`
INSERT INTO organizations
(cached_libelle, cached_nom_complet, id, siret, created_at, updated_at)
VALUES
('Necron', 'Necrontyr', 1, '⚰️', '1967-12-19', '1967-12-19')
;
`;
await pg.sql`
INSERT INTO users
(id, email, created_at, updated_at, given_name, family_name, phone_number, job)
VALUES
(1, '[email protected]', '4444-04-04', '4444-04-04', 'lion', 'el''jonson', 'i', 'primarque')
;
`;
FakeTimers.install({ now: new Date("4444-04-04") });

const userOrganizationLink = await linkUserToOrganization({
organization_id: 1,
user_id: 1,
verification_type: "bypassed",
});

expect(userOrganizationLink).to.deep.equal({
created_at: new Date("4444-04-04"),
has_been_greeted: false,
is_external: false,
needs_official_contact_email_verification: false,
official_contact_email_verification_sent_at: null,
official_contact_email_verification_token: null,
organization_id: 1,
updated_at: new Date("4444-04-04"),
user_id: 1,
verification_type: "bypassed",
verified_at: null,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { DatabaseContext, UserOrganizationLink } from "#src/types";
import type { QueryResult } from "pg";

export function linkUserToOrganizationFactory({ pg }: DatabaseContext) {
return async function linkUserToOrganization({
organization_id,
user_id,
is_external = false,
verification_type,
needs_official_contact_email_verification = false,
}: {
organization_id: number;
user_id: number;
is_external?: boolean;
verification_type: UserOrganizationLink["verification_type"];
needs_official_contact_email_verification?: UserOrganizationLink["needs_official_contact_email_verification"];
}) {
const { rows }: QueryResult<UserOrganizationLink> = await pg.query(
`
INSERT INTO users_organizations
(user_id,
organization_id,
is_external,
verification_type,
needs_official_contact_email_verification,
updated_at,
created_at)
VALUES
($1, $2, $3, $4, $5, $6, $7)
RETURNING *
`,
[
user_id,
organization_id,
is_external,
verification_type,
needs_official_contact_email_verification,
new Date(),
new Date(),
],
);

return rows.shift()! as UserOrganizationLink;
};
}

export type LinkUserToOrganizationHandler = ReturnType<
typeof linkUserToOrganizationFactory
>;
2 changes: 2 additions & 0 deletions packages/identite/src/repositories/organization/upsert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,5 @@ export function upsertFactory({ pg }: DatabaseContext) {
return rows.shift()!;
};
}

export type UpsertHandler = ReturnType<typeof upsertFactory>;
2 changes: 2 additions & 0 deletions packages/identite/src/repositories/user/find-by-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export function findByIdFactory({ pg }: DatabaseContext) {
return rows.shift();
};
}

export type FindByIdHandler = ReturnType<typeof findByIdFactory>;
2 changes: 0 additions & 2 deletions src/config/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ export class InseeNotActiveError extends Error {}

export class UserNotFoundError extends Error {}

export class NotFoundError extends Error {}

export class ForbiddenError extends Error {}

export class UnableToAutoJoinOrganizationError extends Error {
Expand Down
7 changes: 2 additions & 5 deletions src/controllers/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type { NextFunction, Request, Response } from "express";
import HttpErrors from "http-errors";
import { z, ZodError } from "zod";
import {
InseeConnectionError,
InseeNotFoundError,
NotFoundError,
} from "../config/errors";
import { InseeConnectionError, InseeNotFoundError } from "../config/errors";
import notificationMessages from "../config/notification-messages";
import { getOrganizationInfo } from "../connectors/api-sirene";
import { sendModerationProcessedEmail } from "../managers/moderation";
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/organization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getEmailDomain } from "@gouvfr-lasuite/proconnect.core/services/email";
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type { NextFunction, Request, Response } from "express";
import HttpErrors from "http-errors";
import { isEmpty } from "lodash-es";
Expand All @@ -7,7 +8,6 @@ import {
InseeConnectionError,
InseeNotActiveError,
InvalidSiretError,
NotFoundError,
UnableToAutoJoinOrganizationError,
UserAlreadyAskedToJoinOrganizationError,
UserInOrganizationAlreadyError,
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/totp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type { NextFunction, Request, Response } from "express";
import { z } from "zod";
import { InvalidTotpTokenError, NotFoundError } from "../config/errors";
import { InvalidTotpTokenError } from "../config/errors";
import {
addAuthenticationMethodReferenceInSession,
getUserFromAuthenticatedSession,
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/user/edit-moderation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type { NextFunction, Request, Response } from "express";
import { z } from "zod";
import { NotFoundError } from "../../config/errors";
import { cancelModeration } from "../../managers/moderation";
import { getUserFromAuthenticatedSession } from "../../managers/session/authenticated";
import { idSchema } from "../../services/custom-zod-schemas";
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/webauthn.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type {
AuthenticationResponseJSON,
RegistrationResponseJSON,
Expand All @@ -6,7 +7,6 @@ import type { NextFunction, Request, Response } from "express";
import HttpErrors from "http-errors";
import { z, ZodError } from "zod";
import {
NotFoundError,
UserNotLoggedInError,
WebauthnAuthenticationFailedError,
WebauthnRegistrationFailedError,
Expand Down
3 changes: 2 additions & 1 deletion src/managers/moderation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ModerationProcessed } from "@gouvfr-lasuite/proconnect.email";
import { NotFoundError } from "@gouvfr-lasuite/proconnect.identite/errors";
import type { User } from "@gouvfr-lasuite/proconnect.identite/types";
import { isEmpty } from "lodash-es";
import { HOST } from "../config/env";
import { ForbiddenError, NotFoundError } from "../config/errors";
import { ForbiddenError } from "../config/errors";
import { sendMail } from "../connectors/mail";
import {
deleteModeration,
Expand Down
Loading

0 comments on commit c6a6098

Please sign in to comment.