1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Improve test coverage and modernize style for interactive-auth (#2574)

* style: address no-mixed-operators errors,minor style improvements

* test: Fix async interactive-auth tests, add test case

* tests: Fix incorrectly stringified mock response

* pushprocessor: style update

* use async primitives in interactive-auth-spec

* lint

* fixup: remove duplicate test

* add test case for no-flow-with-session for interactive-auth

* interactive-auth: handle non-existing error.data

* async test fix

* test: add dummyauth test

* add testing for errcode

* Revert "pushprocessor: style update"

This reverts commit 3ed0fdfb73.

* add testcase for missing error data

* test: move sessionId assignment

* Add tests to improve coverage for interactive-auth

* pushprocessor: style update
This commit is contained in:
3nprob
2022-08-11 14:29:53 +00:00
committed by GitHub
parent 478270b225
commit 3f6f5b69c7
7 changed files with 374 additions and 103 deletions

View File

@@ -153,27 +153,26 @@ describe("AutoDiscovery", function() {
]); ]);
}); });
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() { it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
const httpBackend = getHttpBackend(); const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc"); httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => { AutoDiscovery.findClientConfig("example.org").then(
const expected = { expect(expected).toEqual,
"m.homeserver": { ),
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]); ]);
}); });

View File

@@ -32,8 +32,8 @@ class FakeClient {
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient; const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
describe("InteractiveAuth", function() { describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", function() { it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn(); const doRequest = jest.fn();
const stateUpdated = jest.fn(); const stateUpdated = jest.fn();
@@ -59,7 +59,7 @@ describe("InteractiveAuth", function() {
}); });
// first we expect a call here // first we expect a call here
stateUpdated.mockImplementation(function(stage) { stateUpdated.mockImplementation((stage) => {
logger.log('aaaa'); logger.log('aaaa');
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({ ia.submitAuthDict({
@@ -69,23 +69,130 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here // .. which should trigger a call here
const requestRes = { "a": "b" }; const requestRes = { "a": "b" };
doRequest.mockImplementation(function(authData) { doRequest.mockImplementation(async (authData) => {
logger.log('cccc'); logger.log('cccc');
expect(authData).toEqual({ expect(authData).toEqual({
session: "sessionId", session: "sessionId",
type: AuthType.Password, type: AuthType.Password,
}); });
return Promise.resolve(requestRes); return requestRes;
}); });
return ia.attemptAuth().then(function(res) { const res = await ia.attemptAuth();
expect(res).toBe(requestRes); expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1); expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1); expect(stateUpdated).toBeCalledTimes(1);
});
}); });
it("should make a request if no authdata is provided", function() { it("should handle auth errcode presence ", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
doRequest,
stateUpdated,
requestEmailToken,
matrixClient: getFakeClient(),
emailSid: 'myEmailSid',
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Email, AuthType.Password] },
],
params: {
[AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Email)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('husky');
expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({
type: AuthType.Email,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('barfoo');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Email,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn(); const doRequest = jest.fn();
const stateUpdated = jest.fn(); const stateUpdated = jest.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = jest.fn();
@@ -101,7 +208,7 @@ describe("InteractiveAuth", function() {
expect(ia.getStageParams(AuthType.Password)).toBe(undefined); expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest // first we expect a call to doRequest
doRequest.mockImplementation(function(authData) { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({ const err = new MatrixError({
@@ -119,7 +226,7 @@ describe("InteractiveAuth", function() {
// .. which should be followed by a call to stateUpdated // .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" }; const requestRes = { "a": "b" };
stateUpdated.mockImplementation(function(stage) { stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password); expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId"); expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({ expect(ia.getStageParams(AuthType.Password)).toEqual({
@@ -127,13 +234,13 @@ describe("InteractiveAuth", function() {
}); });
// submitAuthDict should trigger another call to doRequest // submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(function(authData) { doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData); logger.log("request2", authData);
expect(authData).toEqual({ expect(authData).toEqual({
session: "sessionId", session: "sessionId",
type: AuthType.Password, type: AuthType.Password,
}); });
return Promise.resolve(requestRes); return requestRes;
}); });
ia.submitAuthDict({ ia.submitAuthDict({
@@ -141,14 +248,76 @@ describe("InteractiveAuth", function() {
}); });
}); });
return ia.attemptAuth().then(function(res) { const res = await ia.attemptAuth();
expect(res).toBe(requestRes); expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2); expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1); expect(stateUpdated).toBeCalledTimes(1);
});
}); });
it("should start an auth stage and reject if no auth flow", function() { it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
authData: null,
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn(); const doRequest = jest.fn();
const stateUpdated = jest.fn(); const stateUpdated = jest.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = jest.fn();
@@ -160,7 +329,7 @@ describe("InteractiveAuth", function() {
requestEmailToken, requestEmailToken,
}); });
doRequest.mockImplementation(function(authData) { doRequest.mockImplementation((authData) => {
logger.log("request1", authData); logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({ const err = new MatrixError({
@@ -174,9 +343,108 @@ describe("InteractiveAuth", function() {
throw err; throw err;
}); });
return ia.attemptAuth().catch(function(error) { await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
expect(error.message).toBe('No appropriate authentication flow found'); new Error('No appropriate authentication flow found'),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
},
sessionId: "sessionId",
}); });
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
});
err.httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should handle unexpected error types without data propery set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: "sessionId",
},
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new Error('myerror');
(err as any).httpStatus = 401;
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error("myerror"),
);
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: 'sessionId',
flows: [
{ stages: [AuthType.Dummy] },
],
params: {},
},
});
const requestRes = { "a": "b" };
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Dummy,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
}); });
describe("requestEmailToken", () => { describe("requestEmailToken", () => {
@@ -247,7 +515,7 @@ describe("InteractiveAuth", function() {
doRequest, stateUpdated, requestEmailToken, doRequest, stateUpdated, requestEmailToken,
}); });
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error"); await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
}); });
it("only starts one request at a time", async () => { it("only starts one request at a time", async () => {

View File

@@ -17,6 +17,8 @@ limitations under the License.
/** @module auto-discovery */ /** @module auto-discovery */
import { ServerResponse } from "http";
import { IClientWellKnown, IWellKnownConfig } from "./client"; import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger'; import { logger } from './logger';
@@ -409,39 +411,41 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state. * @return {Promise<object>} Resolves to the returned state.
* @private * @private
*/ */
private static fetchWellKnownObject(url: string): Promise<IWellKnownConfig> { private static fetchWellKnownObject(uri: string): Promise<IWellKnownConfig> {
return new Promise(function(resolve) { return new Promise((resolve) => {
// eslint-disable-next-line // eslint-disable-next-line
const request = require("./matrix").getRequest(); const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available"); if (!request) throw new Error("No request library available");
request( request(
{ method: "GET", uri: url, timeout: 5000 }, { method: "GET", uri, timeout: 5000 },
(err, response, body) => { (error: Error, response: ServerResponse, body: string) => {
if (err || response && if (error || response?.statusCode < 200 || response?.statusCode >= 300) {
(response.statusCode < 200 || response.statusCode >= 300) const result = { error, raw: {} };
) { return resolve(response?.statusCode === 404
let action = AutoDiscoveryAction.FAIL_PROMPT; ? {
let reason = (err ? err.message : null) || "General failure"; ...result,
if (response && response.statusCode === 404) { action: AutoDiscoveryAction.IGNORE,
action = AutoDiscoveryAction.IGNORE; reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN; } : {
} ...result,
resolve({ raw: {}, action: action, reason: reason, error: err }); action: AutoDiscoveryAction.FAIL_PROMPT,
return; reason: error?.message || "General failure",
});
} }
try { try {
resolve({ raw: JSON.parse(body), action: AutoDiscoveryAction.SUCCESS }); return resolve({
} catch (e) { raw: JSON.parse(body),
let reason = AutoDiscovery.ERROR_INVALID; action: AutoDiscoveryAction.SUCCESS,
if (e.name === "SyntaxError") { });
reason = AutoDiscovery.ERROR_INVALID_JSON; } catch (err) {
} return resolve({
resolve({ error: err,
raw: {}, raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT, action: AutoDiscoveryAction.FAIL_PROMPT,
reason: reason, reason: err?.name === "SyntaxError"
error: e, ? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
}); });
} }
}, },

View File

@@ -250,12 +250,9 @@ export class InteractiveAuth {
if (!this.data?.flows) { if (!this.data?.flows) {
this.busyChangedCallback?.(true); this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present. // use the existing sessionId, if one is present.
let auth = null; const auth = this.data.session
if (this.data.session) { ? { session: this.data.session }
auth = { : null;
session: this.data.session,
};
}
this.doRequest(auth).finally(() => { this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false); this.busyChangedCallback?.(false);
}); });
@@ -312,7 +309,7 @@ export class InteractiveAuth {
* @return {string} session id * @return {string} session id
*/ */
public getSessionId(): string { public getSessionId(): string {
return this.data ? this.data.session : undefined; return this.data?.session;
} }
/** /**
@@ -477,6 +474,9 @@ export class InteractiveAuth {
logger.log("Background poll request failed doing UI auth: ignoring", error); logger.log("Background poll request failed doing UI auth: ignoring", error);
} }
} }
if (!error.data) {
error.data = {};
}
// if the error didn't come with flows, completed flows or session ID, // if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without // copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email // any UI auth data (eg. when polling for email validation, if the email
@@ -539,19 +539,17 @@ export class InteractiveAuth {
return; return;
} }
if (this.data && this.data.errcode || this.data.error) { if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, { this.stateUpdatedCallback(nextStage, {
errcode: this.data.errcode || "", errcode: this.data?.errcode || "",
error: this.data.error || "", error: this.data?.error || "",
}); });
return; return;
} }
const stageStatus: IStageStatus = {}; this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE
if (nextStage == EMAIL_STAGE_TYPE) { ? { emailSid: this.emailSid }
stageStatus.emailSid = this.emailSid; : {});
}
this.stateUpdatedCallback(nextStage, stageStatus);
} }
/** /**

View File

@@ -362,13 +362,9 @@ export class PushProcessor {
return false; return false;
} }
let regex; const regex = cond.key === 'content.body'
? this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)')
if (cond.key == 'content.body') { : this.createCachedRegex('^', cond.pattern, '$');
regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = this.createCachedRegex('^', cond.pattern, '$');
}
return !!val.match(regex); return !!val.match(regex);
} }

View File

@@ -328,22 +328,28 @@ export function escapeRegExp(string: string): string {
} }
export function globToRegexp(glob: string, extended?: any): string { export function globToRegexp(glob: string, extended?: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From // From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies, // Because micromatch is about 130KB with dependencies,
// and minimatch is not much better. // and minimatch is not much better.
let pat = escapeRegExp(glob); const replacements: ([RegExp, string | ((substring: string, ...args: any[]) => string) ])[] = [
pat = pat.replace(/\\\*/g, '.*'); [/\\\*/g, '.*'],
pat = pat.replace(/\?/g, '.'); [/\?/g, '.'],
if (extended) { extended !== false && [
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) { /\\\[(!|)(.*)\\]/g,
const first = p1 && '^' || ''; (_match: string, neg: string, pat: string) => [
const second = p2.replace(/\\-/, '-'); '[',
return '[' + first + second + ']'; neg ? '^' : '',
}); pat.replace(/\\-/, '-'),
} ']',
return pat; ].join(''),
],
];
return replacements.reduce(
// https://github.com/microsoft/TypeScript/issues/30134
(pat, args) => args ? pat.replace(args[0], args[1] as any) : pat,
escapeRegExp(glob),
);
} }
export function ensureNoTrailingSlash(url: string): string { export function ensureNoTrailingSlash(url: string): string {

View File

@@ -1865,10 +1865,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const shouldTerminate = ( const shouldTerminate = (
// reject events also end the call if it's ringing: it's another of // reject events also end the call if it's ringing: it's another of
// our devices rejecting the call. // our devices rejecting the call.
([CallState.InviteSent, CallState.Ringing].includes(this.state)) || [CallState.InviteSent, CallState.Ringing].includes(this.state) ||
// also if we're in the init state and it's an inbound call, since // also if we're in the init state and it's an inbound call, since
// this means we just haven't entered the ringing state yet // this means we just haven't entered the ringing state yet
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound (this.state === CallState.Fledgling && this.direction === CallDirection.Inbound)
); );
if (shouldTerminate) { if (shouldTerminate) {