Skip to content

Commit

Permalink
feat(ums): reset user's password (#143)
Browse files Browse the repository at this point in the history
* feat(ums): reset user's password

* test: add gateways and flows management API

* test(e2e): add UMS request reset password test cases
  • Loading branch information
marlosin committed Mar 5, 2020
1 parent 80edb88 commit cf0e4a6
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 4 deletions.
8 changes: 8 additions & 0 deletions e2e/config.ts
Expand Up @@ -32,5 +32,13 @@ export const api = {
},
ums: {
schema: `${MANAGEMENT}/${process.env.E2E_PROJECT_ID}/midgard/schema`,
},
gateways: {
create: `${MANAGEMENT}/${process.env.E2E_PROJECT_ID}/enigma/gateways`,
delete: `${MANAGEMENT}/${process.env.E2E_PROJECT_ID}/enigma/gateways/{gateway_id}`,
},
flows: {
create: `${MANAGEMENT}/${process.env.E2E_PROJECT_ID}/enigma/actions`,
delete: `${MANAGEMENT}/${process.env.E2E_PROJECT_ID}/enigma/actions/{flow_id}`,
}
};
42 changes: 42 additions & 0 deletions e2e/management.ts
Expand Up @@ -21,6 +21,14 @@ interface IResource {
id: string;
}

interface IFlow {
name: string;
gateway_id: string;
subject: string;
templates: any[];
event_type: "AfterPasswordResetRequest";
}

/* Get AWS credentials for fileset */
const { AWS_KEY, AWS_SECRET, AWS_BUCKET } = process.env;

Expand Down Expand Up @@ -239,6 +247,40 @@ export class Management {
.then(([{ id }]) => id);
}

public createGateway(name: string, settings: any, type = "SMTP"): Promise<IResource> {
return this.fetch(api.gateways.create, {
method: "POST",
headers: this.headers,
body: JSON.stringify({ name, type, settings }),
})
.then((response: Response) => response.json());
}

public deleteGateway(id: string): Promise<void> {
return this.fetch(api.gateways.delete.replace("{gateway_id}", id), {
method: "DELETE",
headers: this.headers
})
.then((response: Response) => response.json());
}

public createFlow(action: IFlow): Promise<IResource> {
return this.fetch(api.flows.create, {
method: "POST",
headers: this.headers,
body: JSON.stringify(action),
})
.then((response: Response) => response.json());
}

public deleteFlow(id: string): Promise<void> {
return this.fetch(api.flows.delete.replace("{flow_id}", id), {
method: "DELETE",
headers: this.headers
})
.then((response: Response) => response.json());
}

private fetch(url: string, init: any = {}): Promise<Response> {
return fetch(url, init).then((res) => Management.checkStatus(res, init));
}
Expand Down
80 changes: 80 additions & 0 deletions e2e/stories/ums/ums.e2e.ts
Expand Up @@ -162,6 +162,86 @@ describe("User Management Service", () => {
});
});

describe("reset password", () => {
let gateway: { id: string };
let flow: { id: string };

const resetCredentials = {
email: "test@jexia.com", // avoid sending external e-mails
password: faker.internet.password(),
};

beforeAll(async () => await ums.signUp(resetCredentials));
afterAll(async () => {
await management.deleteFlow(flow.id);
await management.deleteGateway(gateway.id);
await ums.deleteUser(resetCredentials.email, resetCredentials.password);
});

describe("when project has NO gateway nor flow setup", () => {
it("should throw error", async (done) => {
try {
await ums.requestResetPassword(resetCredentials.email);
done.fail("request password reset should not succeed without gateway/flow setup");
} catch (err) {
expect(err.httpStatus.code).toBe(500);
} finally {
done();
}
});
});

describe("when project has NO flow setup", () => {
beforeAll(async () => {
gateway = await management.createGateway(faker.random.word(), {
smtp_server: faker.internet.ip(),
username: resetCredentials.email,
password: resetCredentials.password,
smtp_port: faker.helpers.randomize([465, 587]),
});
});

it("should throw error", async (done) => {
try {
await ums.requestResetPassword(resetCredentials.email);
done.fail("request password reset should not succeed without flow setup");
} catch (err) {
expect(err.httpStatus.code).toBe(500);
} finally {
done();
}
});
});

describe("when project has both gateway and flow setup", () => {
beforeAll(async () => {
flow = await management.createFlow({
name: faker.random.word(),
gateway_id: gateway.id,
subject: faker.lorem.sentence(),
templates: [
{
type: "text/html",
body: "Hello {{.email}}, use the following code in order to reset your password: {{.token}}",
},
],
event_type: "AfterPasswordResetRequest",
});
});

it("should request for reset password successfully", async (done) => {
try {
await ums.requestResetPassword(resetCredentials.email);
expect(true).toBeTruthy();
} catch (err) {
done.fail(err);
} finally {
done();
}
});
});
});

});

describe("initialize with API key", () => {
Expand Down
16 changes: 16 additions & 0 deletions extra_docs/ums/README.md
Expand Up @@ -165,3 +165,19 @@ ums.delete()
.where(field => field("active").isEqualTo(false))
.subscribe();
```

### Change/reset user password
User's password can be changed whether the current password is known by calling:
```javascript
ums.changePassword('Elon@tesla.com', currentPassword, newPassword);
```
\
When the current password isn't known, you need to request a password reset:
```javascript
ums.requestResetPassword('Elon@tesla.com');
```
\
If provided email is valid and exists the user will receive an e-mail with instructions to reset their password. This e-mail will include a reset token, that should be used to reset his password:
```javascript
ums.resetPassword(receivedResetToken, newPassword);
```
47 changes: 46 additions & 1 deletion src/api/ums/umsModule.spec.ts
Expand Up @@ -14,7 +14,7 @@ import { AuthOptions, TokenManager } from "../core/tokenManager";
import { UMSModule } from "./umsModule";

describe("UMS Module", () => {
const projectID = "projectIDTest";
const projectID = faker.random.uuid();
const tokenTest = "tokenTest";
const testUser = {
email: faker.internet.email(),
Expand Down Expand Up @@ -64,6 +64,13 @@ describe("UMS Module", () => {
};
}

it("should get a base path based on project id", async () => {
const { subject, systemDefer, init } = createSubject();
systemDefer.resolve();
await init();
expect(subject.basePath).toEqual(`${API.PROTOCOL}://${projectID}.${API.HOST}.${API.DOMAIN}:${API.PORT}`);
});

describe("on initialize", () => {

it("should get tokenManager from the injector", async () => {
Expand Down Expand Up @@ -282,6 +289,44 @@ describe("UMS Module", () => {
);
});
});

describe("request user's password reset", () => {
it("should call API with correct parameters", async () => {
const { subject, requestAdapterMock, systemDefer, init } = createSubject();
const email = faker.internet.email();
systemDefer.resolve();
await init();
await subject.requestResetPassword(email);

expect(requestAdapterMock.execute).toHaveBeenCalledWith(
`${subject.basePath}/${API.UMS.ENDPOINT}/${API.UMS.RESETPASSWORD}`,
{
body: { email },
method: RequestMethod.POST,
},
);
});
});

describe("when request user's password reset", () => {
it("should call API with correct parameters", async () => {
const { subject, requestAdapterMock, systemDefer, init } = createSubject();
const token = faker.random.alphaNumeric(12);
const newPassword = faker.random.alphaNumeric(12);

systemDefer.resolve();
await init();
await subject.resetPassword(token, newPassword);

expect(requestAdapterMock.execute).toHaveBeenCalledWith(
`${subject.basePath}/${API.UMS.ENDPOINT}/${API.UMS.RESETPASSWORD}/${token}`,
{
body: { new_password: newPassword },
method: RequestMethod.POST,
},
);
});
});
});

});
35 changes: 32 additions & 3 deletions src/api/ums/umsModule.ts
Expand Up @@ -67,6 +67,10 @@ export class UMSModule<
return Promise.resolve(this);
}

public get basePath(): string {
return `${API.PROTOCOL}://${this.projectId}.${API.HOST}.${API.DOMAIN}:${API.PORT}`;
}

/**
* Return configuration
*/
Expand Down Expand Up @@ -200,13 +204,38 @@ export class UMSModule<
return new DeleteQuery(this.requestExecuter, this.resourceType, this.name);
}

/**
* Requests a password reset for the given user e-mail.
* The user should receive an e-mail message with instructions.
* @param email The e-mail address of the user to be reset.
*/
public requestResetPassword(email: string): Promise<D> {
return this.requestAdapter.execute<D>(
this.getUrl(API.UMS.RESETPASSWORD),
{ body: { email }, method: RequestMethod.POST },
);
}

/**
* Resets user's password to a new one
* @param resetToken The reset token the user received
* @param newPassword the new password to be set
*/
public resetPassword(resetToken: string, newPassword: string): Promise<D> {
const body = { new_password: newPassword };
return this.requestAdapter.execute<D>(
this.getUrl(API.UMS.RESETPASSWORD) + `/${resetToken}`,
{ body, method: RequestMethod.POST },
);
}

/**
* Generate API url
* @param api {string} API endpoint
* @param ums {boolean} Whether URL is a part of UMS API
* @param api API endpoint
* @param ums Whether URL is a part of UMS API
*/
private getUrl(api: string, ums = true): string {
let url = `${API.PROTOCOL}://${this.projectId}.${API.HOST}.${API.DOMAIN}:${API.PORT}`;
let url = this.basePath;
if (ums) {
url += `/${API.UMS.ENDPOINT}`;
}
Expand Down
1 change: 1 addition & 0 deletions src/config/config.ts
Expand Up @@ -18,6 +18,7 @@ export const API = {
SIGNUP: "signup",
USER: "user",
CHANGEPASSWORD: "changepassword",
RESETPASSWORD: "resetpassword",
},
CHANNEL: {
ENDPOINT: "channel",
Expand Down

0 comments on commit cf0e4a6

Please sign in to comment.