@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.
Files changed (48) hide show
  1. package/.vscode/settings.json +21 -0
  2. package/CHANGELOG.md +139 -0
  3. package/COMPOSITE_AUTH_SERVICE.md +104 -0
  4. package/package.json +1 -1
  5. package/src/application/policies/account-already-exists.policy.ts +23 -0
  6. package/src/application/policies/magic-link-token.policy.ts +23 -0
  7. package/src/application/policies/oauth-code.policy.ts +23 -0
  8. package/src/application/policies/oauth-provider.policy.ts +29 -0
  9. package/src/application/policies/otp-code-format.policy.ts +23 -0
  10. package/src/application/policies/otp-code.policy.ts +23 -0
  11. package/src/application/policies/otp-identifier.policy.ts +23 -0
  12. package/src/application/policies/password-identifier.policy.ts +23 -0
  13. package/src/application/policies/password-min-length.policy.ts +29 -0
  14. package/src/application/policies/password-present.policy.ts +23 -0
  15. package/src/application/policies/password-strength.policy.ts +27 -0
  16. package/src/application/ports/auth-preference-service.port.ts +14 -0
  17. package/src/application/ports/auth-service.port.ts +58 -11
  18. package/src/application/ports/pre-register-policy.port.ts +7 -0
  19. package/src/application/ports/user-existence.port.ts +3 -0
  20. package/src/application/services/composite-auth.service.ts +96 -0
  21. package/src/application/use-cases/authenticate-user.ts +17 -4
  22. package/src/application/use-cases/register-user.ts +28 -0
  23. package/src/auth-engine.factory.ts +53 -0
  24. package/src/auth-engine.ts +25 -0
  25. package/src/domain/user-account.ts +3 -9
  26. package/src/index.ts +16 -2
  27. package/tests/application/policies/magic-link-token.policy.test.ts +74 -0
  28. package/tests/application/policies/oauth-code.policy.test.ts +79 -0
  29. package/tests/application/policies/oauth-provider.policy.test.ts +76 -0
  30. package/tests/application/policies/otp-code-format.policy.test.ts +90 -0
  31. package/tests/application/policies/otp-code.policy.test.ts +55 -0
  32. package/tests/application/policies/otp-identifier.policy.test.ts +55 -0
  33. package/tests/application/policies/password-identifier.policy.test.ts +55 -0
  34. package/tests/application/policies/password-min-length.policy.test.ts +66 -0
  35. package/tests/application/policies/password-present.policy.test.ts +55 -0
  36. package/tests/application/policies/password-strength.policy.test.ts +66 -0
  37. package/tests/application/ports/auth-preferences-service.port.mock.ts +14 -0
  38. package/tests/application/ports/auth-service.port.mock.ts +19 -0
  39. package/tests/application/ports/pre-register-policy.port.mock.ts +16 -0
  40. package/tests/application/services/composite-auth.mock.ts +18 -0
  41. package/tests/application/services/composite-auth.test.ts +162 -0
  42. package/tests/application/use-cases/authenticate-user.test.ts +61 -14
  43. package/tests/application/use-cases/register-user.test.ts +89 -0
  44. package/tests/domain/user-account.data.ts +5 -0
  45. package/tsconfig.lib.json +4 -1
  46. package/tsconfig.spec.json +13 -1
  47. package/vite.config.mts +4 -1
  48. package/src/domain/session.ts +0 -16
@@ -0,0 +1,96 @@
1
+ import type {
2
+ AuthMethod,
3
+ AuthResult,
4
+ AuthServicePort,
5
+ } from '@application/ports/auth-service.port.js';
6
+
7
+ export type AuthMethodConfig = Partial<
8
+ Record<AuthMethod['type'], AuthServicePort>
9
+ >;
10
+
11
+ export class CompositeAuthService implements AuthServicePort {
12
+ private readonly methodConfig: AuthMethodConfig;
13
+ private readonly adapters: AuthServicePort[];
14
+
15
+ constructor(config: AuthMethodConfig) {
16
+ this.methodConfig = config;
17
+ // Extract all adapters from config for operations like logout
18
+ this.adapters = Object.values(config).filter(
19
+ (adapter): adapter is AuthServicePort => adapter !== undefined
20
+ );
21
+ }
22
+
23
+ async authenticate(method: AuthMethod): Promise<AuthResult> {
24
+ const adapter = this.getAdapterForMethod(method);
25
+
26
+ if (!adapter) {
27
+ return {
28
+ ok: false,
29
+ error: 'ADAPTER_UNAVAILABLE',
30
+ violatedPolicies: [],
31
+ };
32
+ }
33
+
34
+ try {
35
+ return await adapter.authenticate(method);
36
+ } catch (error) {
37
+ console.error('Authentication error in adapter:', error);
38
+ return {
39
+ ok: false,
40
+ error: 'NETWORK_ERROR',
41
+ violatedPolicies: [],
42
+ };
43
+ }
44
+ }
45
+
46
+ async register(method: AuthMethod): Promise<AuthResult> {
47
+ const adapter = this.getAdapterForMethod(method);
48
+
49
+ if (!adapter) {
50
+ return {
51
+ ok: false,
52
+ error: 'ADAPTER_UNAVAILABLE',
53
+ violatedPolicies: [],
54
+ };
55
+ }
56
+
57
+ try {
58
+ return await adapter.register(method);
59
+ } catch (error) {
60
+ console.error('Registration error in adapter:', error);
61
+ return {
62
+ ok: false,
63
+ error: 'NETWORK_ERROR',
64
+ violatedPolicies: [],
65
+ };
66
+ }
67
+ }
68
+
69
+ async logout(): Promise<void> {
70
+ // Try to logout from all adapters
71
+ const logoutPromises = this.adapters.map(async (adapter) => {
72
+ try {
73
+ await adapter.logout();
74
+ } catch (error) {
75
+ console.error('Logout error in adapter:', error);
76
+ // Continue with other adapters even if one fails
77
+ }
78
+ });
79
+
80
+ await Promise.allSettled(logoutPromises);
81
+ }
82
+
83
+ /**
84
+ * Get adapter for specific auth method
85
+ */
86
+ private getAdapterForMethod(method: AuthMethod): AuthServicePort | undefined {
87
+ return this.methodConfig[method.type];
88
+ }
89
+
90
+ /**
91
+ * Get method configuration (read-only)
92
+ */
93
+ getMethodConfig(): AuthMethodConfig {
94
+ return { ...this.methodConfig };
95
+ }
96
+ }
@@ -1,8 +1,21 @@
1
+ import { AuthPreferencesServicePort } from "@application/ports/auth-preference-service.port.js";
2
+ import type { AuthMethod, AuthResult } from "@application/ports/auth-service.port.js";
1
3
  import { AuthServicePort } from "@application/ports/auth-service.port.js";
2
- import type { AuthMethod } from "@application/ports/auth-service.port.js";
3
4
 
4
5
 
5
6
  export const authenticateUser =
6
- (authService: AuthServicePort) =>
7
- (method: AuthMethod) =>
8
- authService.authenticate(method);
7
+ (authService: AuthServicePort, authPreferencesService: AuthPreferencesServicePort) =>
8
+ async (method: AuthMethod): Promise<AuthResult> => {
9
+ const result = await authService.authenticate(method);
10
+ if (!result.ok) {
11
+ return { ok: false, error: result.error, violatedPolicies: [] };
12
+ }
13
+ if (result.user.status === 'disabled') {
14
+ return { ok: false, error: 'ACCOUNT_DISABLED', violatedPolicies: [] };
15
+ }
16
+ const preferences = await authPreferencesService.getAuthPreferences();
17
+ if (!result.user.isVerified && !preferences.allowLoginWithNotVerifiedAccount) {
18
+ return { ok: false, error: 'ACCOUNT_NOT_VERIFIED', violatedPolicies: [] };
19
+ }
20
+ return { ok: true, user: result.user };
21
+ }
@@ -0,0 +1,28 @@
1
+ import { AuthMethod, AuthResult, AuthServicePort } from "@application/ports/auth-service.port.js";
2
+ import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
3
+
4
+ export const registerUser =
5
+ (authService: AuthServicePort, policies: PreRegisterPolicy[]) =>
6
+ async (method: AuthMethod): Promise<AuthResult> => {
7
+ const activePolicies = await Promise.all(policies
8
+ .map(async policy => ({
9
+ isActive: await policy.isActive(method),
10
+ value: policy,
11
+ }))
12
+ );
13
+ const results = await Promise.all(activePolicies
14
+ .filter(policy => policy.isActive)
15
+ .map(async policy => ({
16
+ code: policy.value.code,
17
+ ok: await policy.value.check(method),
18
+ })));
19
+ const violdatedPolicies = results.filter(result => !result.ok);
20
+ if (violdatedPolicies.length > 0) {
21
+ return {
22
+ ok: false,
23
+ error: 'PRE_REGISTER_POLICY_VIOLATED',
24
+ violatedPolicies: violdatedPolicies.map(result => result.code)
25
+ };
26
+ }
27
+ return authService.register(method);
28
+ };
@@ -0,0 +1,53 @@
1
+ import { AccountAlreadyExistsPolicy } from '@application/policies/account-already-exists.policy.js';
2
+ import { OtpCodePolicy } from '@application/policies/otp-code.policy.js';
3
+ import { OtpIdentifierPolicy } from '@application/policies/otp-identifier.policy.js';
4
+ import { PasswordIdentifierPolicy } from '@application/policies/password-identifier.policy.js';
5
+ import { PasswordPresentPolicy } from '@application/policies/password-present.policy.js';
6
+ import { PasswordStrengthPolicy } from '@application/policies/password-strength.policy.js';
7
+ import { AuthPreferencesServicePort } from '@application/ports/auth-preference-service.port.js';
8
+ import { AuthServicePort } from '@application/ports/auth-service.port.js';
9
+ import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
10
+ import { UserExistencePort } from '@application/ports/user-existence.port.js';
11
+ import { authenticateUser } from '@application/use-cases/authenticate-user.js';
12
+ import { registerUser } from '@application/use-cases/register-user.js';
13
+ import { AuthEngine } from '@root/auth-engine.js';
14
+
15
+ export class AuthEngineFactory {
16
+ static async create(
17
+ authService: AuthServicePort,
18
+ authPreferencesService: AuthPreferencesServicePort,
19
+ userExistence: UserExistencePort,
20
+ basePolicies: PreRegisterPolicy[]
21
+ ): Promise<AuthEngine> {
22
+ const preferences = await authPreferencesService.getAuthPreferences();
23
+
24
+ const policies = [
25
+ ...basePolicies,
26
+ new AccountAlreadyExistsPolicy(
27
+ userExistence,
28
+ !preferences.allowDuplicatedAccount
29
+ ),
30
+ new PasswordStrengthPolicy(
31
+ !preferences.allowWeakPassword
32
+ ),
33
+ new PasswordIdentifierPolicy(
34
+ !preferences.allowMissingPasswordIdentifier
35
+ ),
36
+ new PasswordPresentPolicy(
37
+ !preferences.allowMissingPassword
38
+ ),
39
+ new OtpIdentifierPolicy(
40
+ !preferences.allowMissingOtpIdentifier
41
+ ),
42
+ new OtpCodePolicy(
43
+ !preferences.allowMissingOtpCode
44
+ ),
45
+ ];
46
+
47
+ return new AuthEngine({
48
+ register: registerUser(authService, policies),
49
+ authenticate: authenticateUser(authService, authPreferencesService),
50
+ logout: () => authService.logout(),
51
+ });
52
+ }
53
+ }
@@ -0,0 +1,25 @@
1
+ import { AuthMethod, AuthResult } from "@application/ports/auth-service.port.js";
2
+
3
+ export interface AuthActions {
4
+ register(method: AuthMethod): Promise<AuthResult>;
5
+ authenticate(method: AuthMethod): Promise<AuthResult>;
6
+ logout(): Promise<void>;
7
+ }
8
+
9
+ export class AuthEngine implements AuthActions {
10
+ constructor(
11
+ private readonly useCases: AuthActions,
12
+ ) { }
13
+
14
+ async register(method: AuthMethod): Promise<AuthResult> {
15
+ return this.useCases.register(method);
16
+ }
17
+
18
+ async authenticate(method: AuthMethod): Promise<AuthResult> {
19
+ return await this.useCases.authenticate(method);
20
+ }
21
+
22
+ async logout(): Promise<void> {
23
+ return await this.useCases.logout();
24
+ }
25
+ }
@@ -1,22 +1,16 @@
1
1
  export type UserStatus = "active" | "disabled" | "lockedTemporarily";
2
2
 
3
- export type UserRole = "parent" | "staff" | "admin" | "student" | "teacher";
4
-
5
3
  /**
6
4
  * Domaine: compte utilisateur Sekoliko.
7
5
  * Indépendant de toute technologie (pas de dépendance à Supabase ou React).
8
6
  */
9
7
  export interface UserAccount {
10
8
  id: string;
11
- establishmentIds: string[];
12
9
  displayName: string;
13
- firstName?: string;
14
- lastName?: string;
15
- contactEmail: string;
16
10
  status: UserStatus;
17
- roles: UserRole[];
18
- failedLoginAttempts: number;
19
- lastFailedLoginAt: Date | null;
11
+ failedLoginAttempts?: number;
12
+ lastFailedLoginAt?: Date | null;
13
+ isVerified?: boolean;
20
14
  }
21
15
 
22
16
  export const canLogin = (user: UserAccount): boolean =>
package/src/index.ts CHANGED
@@ -2,10 +2,24 @@
2
2
  * Domain
3
3
  */
4
4
  export * from '@domain/user-account.js';
5
- export * from '@domain/session.js';
6
5
 
7
6
  /**
8
7
  * Application
9
8
  */
10
- export * from '@application/use-cases/authenticate-user.js';
9
+ export * from '@application/ports/auth-preference-service.port.js';
11
10
  export * from '@application/ports/auth-service.port.js';
11
+ export * from '@application/ports/pre-register-policy.port.js';
12
+ export * from '@application/ports/user-existence.port.js';
13
+ export * from '@application/services/composite-auth.service.js';
14
+
15
+ /**
16
+ * Infrastructure
17
+ */
18
+ // Infrastructure adapters are provided as separate packages
19
+
20
+ /**
21
+ * Auth Engine
22
+ */
23
+ export * from '@root/auth-engine.factory.js';
24
+ export * from '@root/auth-engine.js';
25
+
@@ -0,0 +1,74 @@
1
+ import { MagicLinkTokenPolicy } from '@application/policies/magic-link-token.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('MagicLinkTokenPolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new MagicLinkTokenPolicy(false);
7
+ const method = {
8
+ type: 'magic_link' as const,
9
+ token: 'short',
10
+ };
11
+
12
+ expect(await policy.isActive(method)).toBe(false);
13
+ });
14
+
15
+ it('should return true when policy is enabled and token meets minimum length', async () => {
16
+ const policy = new MagicLinkTokenPolicy(true);
17
+ const method = {
18
+ type: 'magic_link' as const,
19
+ token: 'valid-token-123456789',
20
+ };
21
+
22
+ expect(await policy.isActive(method)).toBe(true);
23
+ expect(await policy.check(method)).toBe(true);
24
+ });
25
+
26
+ it('should return false when policy is enabled but token is missing', async () => {
27
+ const policy = new MagicLinkTokenPolicy(true);
28
+ const method = {
29
+ type: 'magic_link' as const,
30
+ token: '',
31
+ };
32
+
33
+ expect(await policy.isActive(method)).toBe(true);
34
+ expect(await policy.check(method)).toBe(false);
35
+ });
36
+
37
+ it('should return false when policy is enabled but token is too short', async () => {
38
+ const policy = new MagicLinkTokenPolicy(true);
39
+ const method = {
40
+ type: 'magic_link' as const,
41
+ token: 'short', // 5 chars
42
+ };
43
+
44
+ expect(await policy.isActive(method)).toBe(true);
45
+ expect(await policy.check(method)).toBe(false);
46
+ });
47
+
48
+ it('should return false when policy is enabled but token is exactly at boundary', async () => {
49
+ const policy = new MagicLinkTokenPolicy(true);
50
+ const method = {
51
+ type: 'magic_link' as const,
52
+ token: '1234567890', // exactly 10 chars
53
+ };
54
+
55
+ expect(await policy.isActive(method)).toBe(true);
56
+ expect(await policy.check(method)).toBe(true);
57
+ });
58
+
59
+ it('should return false for non-magic link methods', async () => {
60
+ const policy = new MagicLinkTokenPolicy(true);
61
+ const passwordMethod = {
62
+ type: 'password' as const,
63
+ identifier: 'test@example.com',
64
+ password: 'password123',
65
+ };
66
+
67
+ expect(await policy.isActive(passwordMethod)).toBe(false);
68
+ });
69
+
70
+ it('should have correct error code', () => {
71
+ const policy = new MagicLinkTokenPolicy(true);
72
+ expect(policy.code).toBe('MISSING_MAGIC_LINK_TOKEN');
73
+ });
74
+ });
@@ -0,0 +1,79 @@
1
+ import { OAuthCodePolicy } from '@application/policies/oauth-code.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('OAuthCodePolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new OAuthCodePolicy(false);
7
+ const method = {
8
+ type: 'oauth' as const,
9
+ provider: 'google' as const,
10
+ code: '',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and code meets minimum length', async () => {
17
+ const policy = new OAuthCodePolicy(true);
18
+ const method = {
19
+ type: 'oauth' as const,
20
+ provider: 'google' as const,
21
+ code: 'auth-code-12345',
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 code is missing', async () => {
29
+ const policy = new OAuthCodePolicy(true);
30
+ const method = {
31
+ type: 'oauth' as const,
32
+ provider: 'google' as const,
33
+ code: '',
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 when policy is enabled but code is too short', async () => {
41
+ const policy = new OAuthCodePolicy(true);
42
+ const method = {
43
+ type: 'oauth' as const,
44
+ provider: 'google' as const,
45
+ code: 'five', // 5 chars (too short)
46
+ };
47
+
48
+ expect(await policy.isActive(method)).toBe(true);
49
+ expect(await policy.check(method)).toBe(false);
50
+ });
51
+
52
+ it('should return true when policy is enabled and code is exactly at boundary', async () => {
53
+ const policy = new OAuthCodePolicy(true);
54
+ const method = {
55
+ type: 'oauth' as const,
56
+ provider: 'google' as const,
57
+ code: '12345', // exactly 5 chars
58
+ };
59
+
60
+ expect(await policy.isActive(method)).toBe(true);
61
+ expect(await policy.check(method)).toBe(true);
62
+ });
63
+
64
+ it('should return false for non-OAuth methods', async () => {
65
+ const policy = new OAuthCodePolicy(true);
66
+ const passwordMethod = {
67
+ type: 'password' as const,
68
+ identifier: 'test@example.com',
69
+ password: 'password123',
70
+ };
71
+
72
+ expect(await policy.isActive(passwordMethod)).toBe(false);
73
+ });
74
+
75
+ it('should have correct error code', () => {
76
+ const policy = new OAuthCodePolicy(true);
77
+ expect(policy.code).toBe('MISSING_OAUTH_CODE');
78
+ });
79
+ });
@@ -0,0 +1,76 @@
1
+ import { OAuthProviderPolicy } from '@application/policies/oauth-provider.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('OAuthProviderPolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new OAuthProviderPolicy(false);
7
+ const method = {
8
+ type: 'oauth' as const,
9
+ provider: 'unsupported' as any,
10
+ code: 'auth-code',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and provider is supported', async () => {
17
+ const policy = new OAuthProviderPolicy(true);
18
+ const method = {
19
+ type: 'oauth' as const,
20
+ provider: 'google' as const,
21
+ code: 'auth-code',
22
+ };
23
+
24
+ expect(await policy.isActive(method)).toBe(true);
25
+ expect(await policy.check(method)).toBe(true);
26
+ });
27
+
28
+ it('should return true for all supported providers', async () => {
29
+ const policy = new OAuthProviderPolicy(true);
30
+ const supportedProviders = [
31
+ 'google',
32
+ 'github',
33
+ 'apple',
34
+ 'facebook',
35
+ ] as const;
36
+
37
+ for (const provider of supportedProviders) {
38
+ const method = {
39
+ type: 'oauth' as const,
40
+ provider,
41
+ code: 'auth-code',
42
+ };
43
+
44
+ expect(await policy.isActive(method)).toBe(true);
45
+ expect(await policy.check(method)).toBe(true);
46
+ }
47
+ });
48
+
49
+ it('should return false when policy is enabled but provider is unsupported', async () => {
50
+ const policy = new OAuthProviderPolicy(true);
51
+ const method = {
52
+ type: 'oauth' as const,
53
+ provider: 'unsupported-provider' as any,
54
+ code: 'auth-code',
55
+ };
56
+
57
+ expect(await policy.isActive(method)).toBe(true);
58
+ expect(await policy.check(method)).toBe(false);
59
+ });
60
+
61
+ it('should return false for non-OAuth methods', async () => {
62
+ const policy = new OAuthProviderPolicy(true);
63
+ const passwordMethod = {
64
+ type: 'password' as const,
65
+ identifier: 'test@example.com',
66
+ password: 'password123',
67
+ };
68
+
69
+ expect(await policy.isActive(passwordMethod)).toBe(false);
70
+ });
71
+
72
+ it('should have correct error code', () => {
73
+ const policy = new OAuthProviderPolicy(true);
74
+ expect(policy.code).toBe('UNSUPPORTED_OAUTH_PROVIDER');
75
+ });
76
+ });
@@ -0,0 +1,90 @@
1
+ import { OtpCodeFormatPolicy } from '@application/policies/otp-code-format.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('OtpCodeFormatPolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new OtpCodeFormatPolicy(false);
7
+ const method = {
8
+ type: 'otp' as const,
9
+ identifier: 'test@example.com',
10
+ code: 'invalid',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and code has valid format', async () => {
17
+ const policy = new OtpCodeFormatPolicy(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 code format is invalid', async () => {
29
+ const policy = new OtpCodeFormatPolicy(true);
30
+ const method = {
31
+ type: 'otp' as const,
32
+ identifier: 'test@example.com',
33
+ code: 'abc123', // Contains letters
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 when policy is enabled but code is too short', async () => {
41
+ const policy = new OtpCodeFormatPolicy(true);
42
+ const method = {
43
+ type: 'otp' as const,
44
+ identifier: 'test@example.com',
45
+ code: '12345', // 5 digits
46
+ };
47
+
48
+ expect(await policy.isActive(method)).toBe(true);
49
+ expect(await policy.check(method)).toBe(false);
50
+ });
51
+
52
+ it('should return false when policy is enabled but code is too long', async () => {
53
+ const policy = new OtpCodeFormatPolicy(true);
54
+ const method = {
55
+ type: 'otp' as const,
56
+ identifier: 'test@example.com',
57
+ code: '1234567', // 7 digits
58
+ };
59
+
60
+ expect(await policy.isActive(method)).toBe(true);
61
+ expect(await policy.check(method)).toBe(false);
62
+ });
63
+
64
+ it('should handle empty code string', async () => {
65
+ const policy = new OtpCodeFormatPolicy(true);
66
+ const method = {
67
+ type: 'otp' as const,
68
+ identifier: 'test@example.com',
69
+ code: '',
70
+ };
71
+
72
+ expect(await policy.check(method)).toBe(false);
73
+ });
74
+
75
+ it('should return false for non-OTP methods', async () => {
76
+ const policy = new OtpCodeFormatPolicy(true);
77
+ const passwordMethod = {
78
+ type: 'password' as const,
79
+ identifier: 'test@example.com',
80
+ password: 'password123',
81
+ };
82
+
83
+ expect(await policy.isActive(passwordMethod)).toBe(false);
84
+ });
85
+
86
+ it('should have correct error code', () => {
87
+ const policy = new OtpCodeFormatPolicy(true);
88
+ expect(policy.code).toBe('INVALID_OTP_FORMAT');
89
+ });
90
+ });
@@ -0,0 +1,55 @@
1
+ import { OtpCodePolicy } from '@application/policies/otp-code.policy.js';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('OtpCodePolicy', () => {
5
+ it('should return false when policy is disabled', async () => {
6
+ const policy = new OtpCodePolicy(false);
7
+ const method = {
8
+ type: 'otp' as const,
9
+ identifier: 'test@example.com',
10
+ code: '',
11
+ };
12
+
13
+ expect(await policy.isActive(method)).toBe(false);
14
+ });
15
+
16
+ it('should return true when policy is enabled and code is present', async () => {
17
+ const policy = new OtpCodePolicy(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 code is missing', async () => {
29
+ const policy = new OtpCodePolicy(true);
30
+ const method = {
31
+ type: 'otp' as const,
32
+ identifier: 'test@example.com',
33
+ code: '',
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 OtpCodePolicy(true);
42
+ const passwordMethod = {
43
+ type: 'password' as const,
44
+ identifier: 'test@example.com',
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 OtpCodePolicy(true);
53
+ expect(policy.code).toBe('MISSING_OTP_CODE');
54
+ });
55
+ });