@siran/auth-core 0.1.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +21 -0
- package/CHANGELOG.md +139 -0
- package/COMPOSITE_AUTH_SERVICE.md +104 -0
- package/package.json +1 -1
- package/src/application/policies/account-already-exists.policy.ts +23 -0
- package/src/application/policies/magic-link-token.policy.ts +23 -0
- package/src/application/policies/oauth-code.policy.ts +23 -0
- package/src/application/policies/oauth-provider.policy.ts +29 -0
- package/src/application/policies/otp-code-format.policy.ts +23 -0
- package/src/application/policies/otp-code.policy.ts +23 -0
- package/src/application/policies/otp-identifier.policy.ts +23 -0
- package/src/application/policies/password-identifier.policy.ts +23 -0
- package/src/application/policies/password-min-length.policy.ts +29 -0
- package/src/application/policies/password-present.policy.ts +23 -0
- package/src/application/policies/password-strength.policy.ts +27 -0
- package/src/application/ports/auth-preference-service.port.ts +14 -0
- package/src/application/ports/auth-service.port.ts +58 -11
- package/src/application/ports/pre-register-policy.port.ts +7 -0
- package/src/application/ports/user-existence.port.ts +3 -0
- package/src/application/services/composite-auth.service.ts +96 -0
- package/src/application/use-cases/authenticate-user.ts +17 -4
- package/src/application/use-cases/register-user.ts +28 -0
- package/src/auth-engine.factory.ts +53 -0
- package/src/auth-engine.ts +25 -0
- package/src/domain/user-account.ts +3 -9
- package/src/index.ts +16 -2
- package/tests/application/policies/magic-link-token.policy.test.ts +74 -0
- package/tests/application/policies/oauth-code.policy.test.ts +79 -0
- package/tests/application/policies/oauth-provider.policy.test.ts +76 -0
- package/tests/application/policies/otp-code-format.policy.test.ts +90 -0
- package/tests/application/policies/otp-code.policy.test.ts +55 -0
- package/tests/application/policies/otp-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-min-length.policy.test.ts +66 -0
- package/tests/application/policies/password-present.policy.test.ts +55 -0
- package/tests/application/policies/password-strength.policy.test.ts +66 -0
- package/tests/application/ports/auth-preferences-service.port.mock.ts +14 -0
- package/tests/application/ports/auth-service.port.mock.ts +19 -0
- package/tests/application/ports/pre-register-policy.port.mock.ts +16 -0
- package/tests/application/services/composite-auth.mock.ts +18 -0
- package/tests/application/services/composite-auth.test.ts +162 -0
- package/tests/application/use-cases/authenticate-user.test.ts +61 -14
- package/tests/application/use-cases/register-user.test.ts +89 -0
- package/tests/domain/user-account.data.ts +5 -0
- package/tsconfig.lib.json +4 -1
- package/tsconfig.spec.json +13 -1
- package/vite.config.mts +4 -1
- package/src/domain/session.ts +0 -16
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { OtpIdentifierPolicy } from '@application/policies/otp-identifier.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('OtpIdentifierPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new OtpIdentifierPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'otp' as const,
|
|
9
|
+
identifier: '',
|
|
10
|
+
code: '123456',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and identifier is present', async () => {
|
|
17
|
+
const policy = new OtpIdentifierPolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'otp' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
code: '123456',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but identifier is missing', async () => {
|
|
29
|
+
const policy = new OtpIdentifierPolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'otp' as const,
|
|
32
|
+
identifier: '',
|
|
33
|
+
code: '123456',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false for non-OTP methods', async () => {
|
|
41
|
+
const policy = new OtpIdentifierPolicy(true);
|
|
42
|
+
const passwordMethod = {
|
|
43
|
+
type: 'password' as const,
|
|
44
|
+
identifier: '',
|
|
45
|
+
password: 'password123',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have correct error code', () => {
|
|
52
|
+
const policy = new OtpIdentifierPolicy(true);
|
|
53
|
+
expect(policy.code).toBe('MISSING_OTP_IDENTIFIER');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PasswordIdentifierPolicy } from '@application/policies/password-identifier.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('PasswordIdentifierPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new PasswordIdentifierPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'password' as const,
|
|
9
|
+
identifier: '',
|
|
10
|
+
password: 'password123',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and identifier is present', async () => {
|
|
17
|
+
const policy = new PasswordIdentifierPolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'password' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
password: 'password123',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but identifier is missing', async () => {
|
|
29
|
+
const policy = new PasswordIdentifierPolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'password' as const,
|
|
32
|
+
identifier: '',
|
|
33
|
+
password: 'password123',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false for non-password methods', async () => {
|
|
41
|
+
const policy = new PasswordIdentifierPolicy(true);
|
|
42
|
+
const otpMethod = {
|
|
43
|
+
type: 'otp' as const,
|
|
44
|
+
identifier: '',
|
|
45
|
+
code: '123456',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(otpMethod)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have correct error code', () => {
|
|
52
|
+
const policy = new PasswordIdentifierPolicy(true);
|
|
53
|
+
expect(policy.code).toBe('MISSING_PASSWORD_IDENTIFIER');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PasswordMinLengthPolicy } from '@application/policies/password-min-length.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('PasswordMinLengthPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new PasswordMinLengthPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'password' as const,
|
|
9
|
+
identifier: 'test@example.com',
|
|
10
|
+
password: 'short',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and password meets minimum length', async () => {
|
|
17
|
+
const policy = new PasswordMinLengthPolicy(true, 8);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'password' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
password: 'password123',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but password is too short', async () => {
|
|
29
|
+
const policy = new PasswordMinLengthPolicy(true, 8);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'password' as const,
|
|
32
|
+
identifier: 'test@example.com',
|
|
33
|
+
password: 'short',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should use default minimum length of 8', async () => {
|
|
41
|
+
const policy = new PasswordMinLengthPolicy(true);
|
|
42
|
+
const method = {
|
|
43
|
+
type: 'password' as const,
|
|
44
|
+
identifier: 'test@example.com',
|
|
45
|
+
password: '1234567', // 7 chars
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.check(method)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return false for non-password methods', async () => {
|
|
52
|
+
const policy = new PasswordMinLengthPolicy(true, 8);
|
|
53
|
+
const otpMethod = {
|
|
54
|
+
type: 'otp' as const,
|
|
55
|
+
identifier: 'test@example.com',
|
|
56
|
+
code: '123456',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(await policy.isActive(otpMethod)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should have correct error code', () => {
|
|
63
|
+
const policy = new PasswordMinLengthPolicy(true, 8);
|
|
64
|
+
expect(policy.code).toBe('PASSWORD_TOO_SHORT');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PasswordPresentPolicy } from '@application/policies/password-present.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('PasswordPresentPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new PasswordPresentPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'password' as const,
|
|
9
|
+
identifier: 'test@example.com',
|
|
10
|
+
password: '',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and password is present', async () => {
|
|
17
|
+
const policy = new PasswordPresentPolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'password' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
password: 'password123',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but password is missing', async () => {
|
|
29
|
+
const policy = new PasswordPresentPolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'password' as const,
|
|
32
|
+
identifier: 'test@example.com',
|
|
33
|
+
password: '',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false for non-password methods', async () => {
|
|
41
|
+
const policy = new PasswordPresentPolicy(true);
|
|
42
|
+
const otpMethod = {
|
|
43
|
+
type: 'otp' as const,
|
|
44
|
+
identifier: 'test@example.com',
|
|
45
|
+
code: '123456',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(otpMethod)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have correct error code', () => {
|
|
52
|
+
const policy = new PasswordPresentPolicy(true);
|
|
53
|
+
expect(policy.code).toBe('MISSING_PASSWORD');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PasswordStrengthPolicy } from '@application/policies/password-strength.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('PasswordStrengthPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new PasswordStrengthPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'password' as const,
|
|
9
|
+
identifier: 'test@example.com',
|
|
10
|
+
password: 'weak',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and password meets minimum length', async () => {
|
|
17
|
+
const policy = new PasswordStrengthPolicy(true, 6);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'password' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
password: 'strong123',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but password is too short', async () => {
|
|
29
|
+
const policy = new PasswordStrengthPolicy(true, 12);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'password' as const,
|
|
32
|
+
identifier: 'test@example.com',
|
|
33
|
+
password: 'short',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should use default minimum length of 8', async () => {
|
|
41
|
+
const policy = new PasswordStrengthPolicy(true);
|
|
42
|
+
const method = {
|
|
43
|
+
type: 'password' as const,
|
|
44
|
+
identifier: 'test@example.com',
|
|
45
|
+
password: '1234567', // 7 chars
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.check(method)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return true for non-password methods', async () => {
|
|
52
|
+
const policy = new PasswordStrengthPolicy(true, 8);
|
|
53
|
+
const otpMethod = {
|
|
54
|
+
type: 'otp' as const,
|
|
55
|
+
identifier: 'test@example.com',
|
|
56
|
+
code: '123456',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(await policy.isActive(otpMethod)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should have correct error code', () => {
|
|
63
|
+
const policy = new PasswordStrengthPolicy(true, 8);
|
|
64
|
+
expect(policy.code).toBe('WEAK_PASSWORD');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AuthPreferences, AuthPreferencesServicePort } from "@application/ports/auth-preference-service.port.js";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
interface AuthPreferencesServiceMock {
|
|
5
|
+
getAuthPreferences?: () => Promise<AuthPreferences>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createAuthPreferencesServiceMock(
|
|
9
|
+
authPreferencesService?: AuthPreferencesServiceMock,
|
|
10
|
+
): AuthPreferencesServicePort {
|
|
11
|
+
return {
|
|
12
|
+
getAuthPreferences: authPreferencesService?.getAuthPreferences ?? vi.fn().mockResolvedValue({ allowLoginWithNotVerifiedAccount: false }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AuthMethod, AuthResult, AuthServicePort } from "@application/ports/auth-service.port.js";
|
|
2
|
+
import { UserAccount } from "@domain/user-account.js";
|
|
3
|
+
import { vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const activeUser: UserAccount = { id: '1', displayName: 'test@test.com', status: 'active', isVerified: true };
|
|
6
|
+
|
|
7
|
+
interface AuthServiceMock {
|
|
8
|
+
authenticate?: (method: AuthMethod) => Promise<AuthResult>;
|
|
9
|
+
register?: (method: AuthMethod) => Promise<AuthResult>;
|
|
10
|
+
logout?: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createAuthServiceMock(authService?: AuthServiceMock): AuthServicePort {
|
|
14
|
+
return {
|
|
15
|
+
authenticate: authService?.authenticate ?? vi.fn().mockResolvedValue({ ok: true, user: activeUser }),
|
|
16
|
+
logout: authService?.logout ?? vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
register: authService?.register ?? vi.fn().mockResolvedValue({ ok: true, user: activeUser }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AuthMethod } from "@application/ports/auth-service.port.js";
|
|
2
|
+
import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
|
|
3
|
+
|
|
4
|
+
export interface PreRegisterPolicyMock {
|
|
5
|
+
check?: (method: AuthMethod) => Promise<boolean>;
|
|
6
|
+
isActive?: (method: AuthMethod) => Promise<boolean>;
|
|
7
|
+
code?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createPreRegisterPolicyMock(preRegisterPolicy?: PreRegisterPolicyMock): PreRegisterPolicy {
|
|
11
|
+
return {
|
|
12
|
+
check: preRegisterPolicy?.check ?? vi.fn().mockResolvedValue(true),
|
|
13
|
+
isActive: preRegisterPolicy?.isActive ?? vi.fn().mockResolvedValue(false),
|
|
14
|
+
code: preRegisterPolicy?.code ?? 'POLICY_ERROR',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AuthServicePort } from '@application/ports/auth-service.port.js';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
export function createPasswordAdapterMock(): AuthServicePort {
|
|
5
|
+
return {
|
|
6
|
+
authenticate: vi.fn(),
|
|
7
|
+
register: vi.fn(),
|
|
8
|
+
logout: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createOAuthAdapterMock(): AuthServicePort {
|
|
13
|
+
return {
|
|
14
|
+
authenticate: vi.fn(),
|
|
15
|
+
register: vi.fn(),
|
|
16
|
+
logout: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { AuthServicePort } from '@application/ports/auth-service.port.js';
|
|
2
|
+
import { CompositeAuthService } from '@application/services/composite-auth.service.js';
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
createOAuthAdapterMock,
|
|
6
|
+
createPasswordAdapterMock,
|
|
7
|
+
} from '../services/composite-auth.mock.js';
|
|
8
|
+
|
|
9
|
+
describe('CompositeAuthService', () => {
|
|
10
|
+
let compositeAuthService: CompositeAuthService;
|
|
11
|
+
let mockPasswordAdapter: AuthServicePort;
|
|
12
|
+
let mockOAuthAdapter: AuthServicePort;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockPasswordAdapter = createPasswordAdapterMock();
|
|
16
|
+
mockOAuthAdapter = createOAuthAdapterMock();
|
|
17
|
+
|
|
18
|
+
compositeAuthService = new CompositeAuthService({
|
|
19
|
+
password: mockPasswordAdapter,
|
|
20
|
+
oauth: mockOAuthAdapter,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should create composite service with method-based config', () => {
|
|
25
|
+
expect(compositeAuthService).toBeDefined();
|
|
26
|
+
expect(compositeAuthService.getMethodConfig()).toEqual({
|
|
27
|
+
password: mockPasswordAdapter,
|
|
28
|
+
oauth: mockOAuthAdapter,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should authenticate with password adapter', async () => {
|
|
33
|
+
const mockUser = {
|
|
34
|
+
id: 'test-id',
|
|
35
|
+
displayName: 'Test User',
|
|
36
|
+
status: 'active' as const,
|
|
37
|
+
isVerified: true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
(mockPasswordAdapter.authenticate as any).mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
user: mockUser,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await compositeAuthService.authenticate({
|
|
46
|
+
type: 'password',
|
|
47
|
+
identifier: 'test@example.com',
|
|
48
|
+
password: 'password',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.ok).toBe(true);
|
|
52
|
+
if (result.ok) {
|
|
53
|
+
expect(result.user).toEqual(mockUser);
|
|
54
|
+
}
|
|
55
|
+
expect(mockPasswordAdapter.authenticate).toHaveBeenCalled();
|
|
56
|
+
expect(mockOAuthAdapter.authenticate).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should authenticate with oauth adapter', async () => {
|
|
60
|
+
const mockUser = {
|
|
61
|
+
id: 'test-id',
|
|
62
|
+
displayName: 'Test User',
|
|
63
|
+
status: 'active' as const,
|
|
64
|
+
isVerified: true,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
(mockOAuthAdapter.authenticate as any).mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
user: mockUser,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await compositeAuthService.authenticate({
|
|
73
|
+
type: 'oauth',
|
|
74
|
+
provider: 'google',
|
|
75
|
+
code: 'auth-code',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.ok).toBe(true);
|
|
79
|
+
if (result.ok) {
|
|
80
|
+
expect(result.user).toEqual(mockUser);
|
|
81
|
+
}
|
|
82
|
+
expect(mockOAuthAdapter.authenticate).toHaveBeenCalled();
|
|
83
|
+
expect(mockPasswordAdapter.authenticate).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should validate magic link token format', async () => {
|
|
87
|
+
const result = await compositeAuthService.authenticate({
|
|
88
|
+
type: 'magic_link',
|
|
89
|
+
token: 'short', // Too short
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
expect(result.error).toBe('ADAPTER_UNAVAILABLE'); // No magic_link adapter configured
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return error for unsupported auth method', async () => {
|
|
99
|
+
const result = await compositeAuthService.authenticate({
|
|
100
|
+
type: 'unknown_method' as any, // Unsupported method type
|
|
101
|
+
token: 'magic-token',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.ok).toBe(false);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
expect(result.error).toBe('ADAPTER_UNAVAILABLE');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle register with correct adapter', async () => {
|
|
111
|
+
const mockUser = {
|
|
112
|
+
id: 'test-id',
|
|
113
|
+
displayName: 'Test User',
|
|
114
|
+
status: 'active' as const,
|
|
115
|
+
isVerified: true,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
(mockPasswordAdapter.register as any).mockResolvedValue({
|
|
119
|
+
ok: true,
|
|
120
|
+
user: mockUser,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await compositeAuthService.register({
|
|
124
|
+
type: 'password',
|
|
125
|
+
identifier: 'test@example.com',
|
|
126
|
+
password: 'password',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(result.ok).toBe(true);
|
|
130
|
+
if (result.ok) {
|
|
131
|
+
expect(result.user).toEqual(mockUser);
|
|
132
|
+
}
|
|
133
|
+
expect(mockPasswordAdapter.register).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should validate OTP code format', async () => {
|
|
137
|
+
const result = await compositeAuthService.authenticate({
|
|
138
|
+
type: 'otp',
|
|
139
|
+
identifier: 'test@example.com',
|
|
140
|
+
code: '123', // Too short
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result.ok).toBe(false);
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
expect(result.error).toBe('ADAPTER_UNAVAILABLE'); // No OTP adapter configured
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should demonstrate exact requested syntax', () => {
|
|
150
|
+
// This is exactly how the user wants to use it:
|
|
151
|
+
const engine = new CompositeAuthService({
|
|
152
|
+
password: createPasswordAdapterMock(),
|
|
153
|
+
oauth: createOAuthAdapterMock(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(engine).toBeDefined();
|
|
157
|
+
expect(engine.getMethodConfig()).toEqual({
|
|
158
|
+
password: expect.any(Object),
|
|
159
|
+
oauth: expect.any(Object),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
import { authenticateUser } from "@application/use-cases/authenticate-user.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createAuthPreferencesServiceMock } from "@tests/application/ports/auth-preferences-service.port.mock.js";
|
|
3
|
+
import { createAuthServiceMock } from "@tests/application/ports/auth-service.port.mock.js";
|
|
4
|
+
import { inactiveUser, notVerifiedUser } from "@tests/domain/user-account.data.js";
|
|
3
5
|
import { describe, expect, it, vi } from "vitest";
|
|
4
6
|
|
|
5
7
|
describe('AuthenticateUser', () => {
|
|
6
8
|
it('should authenticate a user', async () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
9
|
+
// Given
|
|
10
|
+
const authService = createAuthServiceMock();
|
|
11
|
+
const authPreferencesService = createAuthPreferencesServiceMock();
|
|
12
|
+
const loginUser = authenticateUser(authService, authPreferencesService);
|
|
13
|
+
// When
|
|
14
|
+
const result = await loginUser({ type: 'password', identifier: 'test@test.com', password: 'test' });
|
|
15
|
+
// Then
|
|
16
|
+
expect(result.ok).toBe(true);
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
it('should return an error if the authentication fails', async () => {
|
|
17
|
-
|
|
20
|
+
// Given
|
|
21
|
+
const authService = createAuthServiceMock({
|
|
18
22
|
authenticate: vi.fn().mockResolvedValue({ ok: false, error: 'INVALID_CREDENTIALS' }),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
});
|
|
24
|
+
const authPreferencesService = createAuthPreferencesServiceMock();
|
|
25
|
+
// When
|
|
26
|
+
const loginUser = authenticateUser(authService, authPreferencesService);
|
|
27
|
+
const user = await loginUser({ type: 'password', identifier: 'test@test.com', password: 'test' });
|
|
28
|
+
// Then
|
|
29
|
+
expect(user).toEqual({ ok: false, error: 'INVALID_CREDENTIALS', violatedPolicies: [] });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should not be possible to authenticate with deactivated account', async () => {
|
|
33
|
+
// Given
|
|
34
|
+
const authService = createAuthServiceMock({
|
|
35
|
+
authenticate: vi.fn().mockResolvedValue({ ok: true, user: inactiveUser }),
|
|
36
|
+
});
|
|
37
|
+
const authPreferencesService = createAuthPreferencesServiceMock();
|
|
38
|
+
// When
|
|
39
|
+
const loginUser = authenticateUser(authService, authPreferencesService);
|
|
22
40
|
const user = await loginUser({ type: 'password', identifier: 'test@test.com', password: 'test' });
|
|
23
|
-
|
|
41
|
+
// Then
|
|
42
|
+
expect(user).toEqual({ ok: false, error: 'ACCOUNT_DISABLED', violatedPolicies: [] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not be possible to authenticate with not verified account', async () => {
|
|
46
|
+
// Given
|
|
47
|
+
const authService = createAuthServiceMock({
|
|
48
|
+
authenticate: vi.fn().mockResolvedValue({ ok: true, user: notVerifiedUser }),
|
|
49
|
+
});
|
|
50
|
+
const authPreferencesService = createAuthPreferencesServiceMock();
|
|
51
|
+
// When
|
|
52
|
+
const loginUser = authenticateUser(authService, authPreferencesService);
|
|
53
|
+
// Then
|
|
54
|
+
const result = await loginUser({ type: 'password', identifier: 'test@test.com', password: 'test' });
|
|
55
|
+
expect(result).toEqual({ ok: false, error: 'ACCOUNT_NOT_VERIFIED', violatedPolicies: [] });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should be possible to authenticate with not verified account if unckeched in settings', async () => {
|
|
59
|
+
// Given
|
|
60
|
+
const authService = createAuthServiceMock({
|
|
61
|
+
authenticate: vi.fn().mockResolvedValue({ ok: true, user: notVerifiedUser }),
|
|
62
|
+
});
|
|
63
|
+
const authPreferencesService = createAuthPreferencesServiceMock({
|
|
64
|
+
getAuthPreferences: vi.fn().mockResolvedValue({ allowLoginWithNotVerifiedAccount: true }),
|
|
65
|
+
});
|
|
66
|
+
// When
|
|
67
|
+
const loginUser = authenticateUser(authService, authPreferencesService);
|
|
68
|
+
// Then
|
|
69
|
+
const result = await loginUser({ type: 'password', identifier: 'test@test.com', password: 'test' });
|
|
70
|
+
expect(result.ok).toEqual(true);
|
|
24
71
|
});
|
|
25
|
-
});
|
|
72
|
+
});
|