diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2f94f29
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,13 @@
+name: CI
+on: [push, pull_request]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - run: npm install
+ - run: npm run build
+ - run: npm run typecheck
diff --git a/package-lock.json b/package-lock.json
index 2440917..5abb1a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
"name": "@authorizerdev/authorizer-vue",
- "version": "1.0.0",
+ "version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@authorizerdev/authorizer-vue",
- "version": "1.0.0",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@authorizerdev/authorizer-js": "^1.2.3"
+ "@authorizerdev/authorizer-js": "^3.2.1"
},
"devDependencies": {
"@babel/types": "^7.21.4",
@@ -35,11 +35,12 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.3.tgz",
- "integrity": "sha512-rk/fMRIsqbp+fsy2y09etVjf7CY9/4mG6hf0RKgXgRRfxtAQa1jdkt/De23hBTNeEwAWu6hP/9BQZjcrln6KtA==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-3.2.1.tgz",
+ "integrity": "sha512-Z7Vpdqs3JsosCcjV63rd7w7mj5n9Vh+RnGDR+qamTbbII+cbwttHpw1jP+61P2uNQLWC3jjp50HvL7xccWAnJQ==",
+ "license": "MIT",
"dependencies": {
- "cross-fetch": "^3.1.5"
+ "cross-fetch": "^4.1.0"
},
"engines": {
"node": ">=16"
@@ -1248,11 +1249,12 @@
"dev": true
},
"node_modules/cross-fetch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
- "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
+ "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
+ "license": "MIT",
"dependencies": {
- "node-fetch": "2.6.7"
+ "node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": {
@@ -2165,9 +2167,10 @@
"dev": true
},
"node_modules/node-fetch": {
- "version": "2.6.7",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
- "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -2670,7 +2673,8 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
},
"node_modules/tslib": {
"version": "1.14.1",
@@ -2875,12 +2879,14 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@@ -2946,11 +2952,11 @@
},
"dependencies": {
"@authorizerdev/authorizer-js": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.3.tgz",
- "integrity": "sha512-rk/fMRIsqbp+fsy2y09etVjf7CY9/4mG6hf0RKgXgRRfxtAQa1jdkt/De23hBTNeEwAWu6hP/9BQZjcrln6KtA==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-3.2.1.tgz",
+ "integrity": "sha512-Z7Vpdqs3JsosCcjV63rd7w7mj5n9Vh+RnGDR+qamTbbII+cbwttHpw1jP+61P2uNQLWC3jjp50HvL7xccWAnJQ==",
"requires": {
- "cross-fetch": "^3.1.5"
+ "cross-fetch": "^4.1.0"
}
},
"@babel/helper-string-parser": {
@@ -3410,7 +3416,8 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz",
"integrity": "sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"@volar/language-core": {
"version": "1.4.0",
@@ -3597,7 +3604,8 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"ajv": {
"version": "6.12.6",
@@ -3738,11 +3746,11 @@
"dev": true
},
"cross-fetch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
- "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
+ "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"requires": {
- "node-fetch": "2.6.7"
+ "node-fetch": "^2.7.0"
}
},
"cross-spawn": {
@@ -3925,7 +3933,8 @@
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
- "dev": true
+ "dev": true,
+ "requires": {}
},
"eslint-plugin-prettier-vue": {
"version": "4.2.0",
@@ -4429,9 +4438,9 @@
"dev": true
},
"node-fetch": {
- "version": "2.6.7",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
- "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"requires": {
"whatwg-url": "^5.0.0"
}
diff --git a/package.json b/package.json
index aecd6fe..8885a21 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@authorizerdev/authorizer-vue",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "authorizer vue sdk",
"files": [
"dist"
@@ -13,9 +13,9 @@
"build": "vite build && npm run build:types",
"build:types": "vue-tsc --project tsconfig.build-types.json --declaration --emitDeclarationOnly --outDir dist/types ",
"typecheck": "vue-tsc --project tsconfig.build-types.json --noEmit",
+ "test": "vue-tsc --project tsconfig.build-types.json --noEmit",
"lint": "prettier --plugin-search-dir . --check . --ignore-path .gitignore && eslint . --ignore-path .gitignore",
- "format": "prettier --plugin-search-dir . --write . --ignore-path .gitignore",
- "prepare": "husky install"
+ "format": "prettier --plugin-search-dir . --write . --ignore-path .gitignore"
},
"keywords": [],
"author": "Lakhan Samani",
@@ -47,6 +47,6 @@
"vue-tsc": "^1.4.2"
},
"dependencies": {
- "@authorizerdev/authorizer-js": "^1.2.3"
+ "@authorizerdev/authorizer-js": "^3.2.1"
}
}
diff --git a/src/__tests__/AuthorizerBasicAuthLogin.test.ts b/src/__tests__/AuthorizerBasicAuthLogin.test.ts
new file mode 100644
index 0000000..bc5c56a
--- /dev/null
+++ b/src/__tests__/AuthorizerBasicAuthLogin.test.ts
@@ -0,0 +1,112 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerBasicAuthLogin from '../components/AuthorizerBasicAuthLogin.vue';
+import globalContext from '../state/globalContext';
+import globalConfig from '../state/globalConfig';
+
+const { mockLogin, mockVerifyOtp, mockResendOtp } = vi.hoisted(() => ({
+ mockLogin: vi.fn(),
+ mockVerifyOtp: vi.fn(),
+ mockResendOtp: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ login: mockLogin,
+ verifyOtp: mockVerifyOtp,
+ resendOtp: mockResendOtp,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerBasicAuthLogin', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalConfig, {
+ is_sign_up_enabled: true,
+ is_basic_authentication_enabled: true,
+ is_strong_password_enabled: false
+ });
+ globalContext.setAuthData = vi.fn();
+ });
+
+ it('renders email and password fields', () => {
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ expect(wrapper.find('#authorizer-login-email').exists()).toBe(true);
+ expect(wrapper.find('#authorizer-login-password').exists()).toBe(true);
+ });
+
+ it('validates empty email', async () => {
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ const emailInput = wrapper.find('#authorizer-login-email');
+ await emailInput.setValue('');
+ expect(wrapper.text()).toContain('Email is required');
+ });
+
+ it('validates invalid email', async () => {
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ const emailInput = wrapper.find('#authorizer-login-email');
+ await emailInput.setValue('not-an-email');
+ expect(wrapper.text()).toContain('Please enter valid email');
+ });
+
+ it('validates empty password', async () => {
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ const passwordInput = wrapper.find('#authorizer-login-password');
+ await passwordInput.setValue('');
+ expect(wrapper.text()).toContain('Password is required');
+ });
+
+ it('calls login on form submit and handles success', async () => {
+ const mockUser = { id: '1', email: 'test@test.com' };
+ const mockToken = { access_token: 'tok', expires_in: 3600, user: mockUser };
+ mockLogin.mockResolvedValueOnce({ data: mockToken, errors: null });
+
+ const onLogin = vi.fn();
+ const wrapper = mount(AuthorizerBasicAuthLogin, {
+ props: { onLogin }
+ });
+
+ await wrapper.find('#authorizer-login-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-login-password').setValue('password123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockLogin).toHaveBeenCalledWith(
+ expect.objectContaining({ email: 'test@test.com', password: 'password123' })
+ );
+ expect(onLogin).toHaveBeenCalledWith(mockToken);
+ });
+
+ it('handles login errors', async () => {
+ mockLogin.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'Invalid credentials' }]
+ });
+
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ await wrapper.find('#authorizer-login-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-login-password').setValue('wrong');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('Invalid credentials');
+ });
+
+ it('shows OTP screen when MFA is required', async () => {
+ mockLogin.mockResolvedValueOnce({
+ data: { should_show_email_otp_screen: true },
+ errors: null
+ });
+
+ const wrapper = mount(AuthorizerBasicAuthLogin);
+ await wrapper.find('#authorizer-login-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-login-password').setValue('password123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.find('#authorizer-verify-otp').exists()).toBe(true);
+ });
+});
diff --git a/src/__tests__/AuthorizerForgotPassword.test.ts b/src/__tests__/AuthorizerForgotPassword.test.ts
new file mode 100644
index 0000000..f928863
--- /dev/null
+++ b/src/__tests__/AuthorizerForgotPassword.test.ts
@@ -0,0 +1,68 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerForgotPassword from '../components/AuthorizerForgotPassword.vue';
+import globalConfig from '../state/globalConfig';
+
+const { mockForgotPassword } = vi.hoisted(() => ({
+ mockForgotPassword: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ forgotPassword: mockForgotPassword,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerForgotPassword', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalConfig, {
+ redirectURL: 'http://localhost:3000'
+ });
+ });
+
+ it('renders email field', () => {
+ const wrapper = mount(AuthorizerForgotPassword);
+ expect(wrapper.find('#authorizer-forgot-password-email').exists()).toBe(true);
+ });
+
+ it('validates empty email', async () => {
+ const wrapper = mount(AuthorizerForgotPassword);
+ await wrapper.find('#authorizer-forgot-password-email').setValue('');
+ expect(wrapper.text()).toContain('Email is required');
+ });
+
+ it('calls forgotPassword and shows success message', async () => {
+ mockForgotPassword.mockResolvedValueOnce({
+ data: { message: 'Reset link sent to your email' },
+ errors: null
+ });
+
+ const wrapper = mount(AuthorizerForgotPassword);
+ await wrapper.find('#authorizer-forgot-password-email').setValue('test@test.com');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockForgotPassword).toHaveBeenCalledWith(
+ expect.objectContaining({ email: 'test@test.com' })
+ );
+ expect(wrapper.text()).toContain('Reset link sent to your email');
+ });
+
+ it('shows error on forgotPassword failure', async () => {
+ mockForgotPassword.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'User not found' }]
+ });
+
+ const wrapper = mount(AuthorizerForgotPassword);
+ await wrapper.find('#authorizer-forgot-password-email').setValue('test@test.com');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('User not found');
+ });
+});
diff --git a/src/__tests__/AuthorizerMagicLinkLogin.test.ts b/src/__tests__/AuthorizerMagicLinkLogin.test.ts
new file mode 100644
index 0000000..f1efaf0
--- /dev/null
+++ b/src/__tests__/AuthorizerMagicLinkLogin.test.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerMagicLinkLogin from '../components/AuthorizerMagicLinkLogin.vue';
+
+const { mockMagicLinkLogin } = vi.hoisted(() => ({
+ mockMagicLinkLogin: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ magicLinkLogin: mockMagicLinkLogin,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerMagicLinkLogin', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders email field', () => {
+ const wrapper = mount(AuthorizerMagicLinkLogin);
+ expect(wrapper.find('#authorizer-magic-link-login-email').exists()).toBe(true);
+ });
+
+ it('validates empty email', async () => {
+ const wrapper = mount(AuthorizerMagicLinkLogin);
+ await wrapper.find('#authorizer-magic-link-login-email').setValue('');
+ expect(wrapper.text()).toContain('Email is required');
+ });
+
+ it('validates invalid email', async () => {
+ const wrapper = mount(AuthorizerMagicLinkLogin);
+ await wrapper.find('#authorizer-magic-link-login-email').setValue('not-email');
+ expect(wrapper.text()).toContain('Please enter valid email');
+ });
+
+ it('calls magicLinkLogin and shows success message', async () => {
+ mockMagicLinkLogin.mockResolvedValueOnce({
+ data: { message: 'Magic link sent to your email' },
+ errors: null
+ });
+
+ const wrapper = mount(AuthorizerMagicLinkLogin);
+ await wrapper.find('#authorizer-magic-link-login-email').setValue('test@test.com');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockMagicLinkLogin).toHaveBeenCalledWith(
+ expect.objectContaining({ email: 'test@test.com' })
+ );
+ expect(wrapper.text()).toContain('Magic link sent to your email');
+ });
+
+ it('shows error on magicLinkLogin failure', async () => {
+ mockMagicLinkLogin.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'User not found' }]
+ });
+
+ const wrapper = mount(AuthorizerMagicLinkLogin);
+ await wrapper.find('#authorizer-magic-link-login-email').setValue('test@test.com');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('User not found');
+ });
+});
diff --git a/src/__tests__/AuthorizerProvider.test.ts b/src/__tests__/AuthorizerProvider.test.ts
new file mode 100644
index 0000000..b785246
--- /dev/null
+++ b/src/__tests__/AuthorizerProvider.test.ts
@@ -0,0 +1,171 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import { defineComponent, h, inject } from 'vue';
+import AuthorizerProvider from '../components/AuthorizerProvider.vue';
+import globalContext from '../state/globalContext';
+import globalConfig from '../state/globalConfig';
+
+const { mockGetMetaData, mockGetSession, mockLogout } = vi.hoisted(() => ({
+ mockGetMetaData: vi.fn().mockResolvedValue({
+ data: {
+ is_google_login_enabled: true,
+ is_basic_authentication_enabled: true,
+ is_sign_up_enabled: true
+ },
+ errors: null
+ }),
+ mockGetSession: vi.fn().mockResolvedValue({
+ data: null,
+ errors: [{ message: 'unauthorized' }]
+ }),
+ mockLogout: vi.fn().mockResolvedValue({ data: { message: 'logged out' } })
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ getMetaData: mockGetMetaData,
+ getSession: mockGetSession,
+ logout: mockLogout
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+const ChildComponent = defineComponent({
+ setup() {
+ const useAuthorizer = inject('useAuthorizer') as () => unknown;
+ const ctx = useAuthorizer?.();
+ return { ctx };
+ },
+ render() {
+ return h('div', { class: 'child' }, 'Child content');
+ }
+});
+
+describe('AuthorizerProvider', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalContext, {
+ user: null,
+ token: null,
+ loading: false
+ });
+ Object.assign(globalConfig, {
+ authorizerURL: '',
+ redirectURL: '',
+ client_id: ''
+ });
+ mockGetMetaData.mockResolvedValue({
+ data: {
+ is_google_login_enabled: true,
+ is_basic_authentication_enabled: true,
+ is_sign_up_enabled: true
+ },
+ errors: null
+ });
+ mockGetSession.mockResolvedValue({
+ data: null,
+ errors: [{ message: 'unauthorized' }]
+ });
+ });
+
+ it('renders slot content', () => {
+ const wrapper = mount(AuthorizerProvider, {
+ props: {
+ config: {
+ authorizerURL: 'http://localhost:8080',
+ redirectURL: 'http://localhost:3000',
+ clientID: 'test-client'
+ }
+ },
+ slots: {
+ default: () => h('span', 'Hello')
+ }
+ });
+ expect(wrapper.text()).toContain('Hello');
+ });
+
+ it('provides useAuthorizer to children', () => {
+ const wrapper = mount(AuthorizerProvider, {
+ props: {
+ config: {
+ authorizerURL: 'http://localhost:8080',
+ redirectURL: 'http://localhost:3000',
+ clientID: 'test-client'
+ }
+ },
+ slots: {
+ default: () => h(ChildComponent)
+ }
+ });
+ expect(wrapper.find('.child').exists()).toBe(true);
+ });
+
+ it('calls getMetaData and getSession on mount', async () => {
+ mount(AuthorizerProvider, {
+ props: {
+ config: {
+ authorizerURL: 'http://localhost:8080',
+ redirectURL: 'http://localhost:3000',
+ clientID: 'test-client'
+ }
+ },
+ slots: {
+ default: () => h('div', 'test')
+ }
+ });
+ await flushPromises();
+ expect(mockGetMetaData).toHaveBeenCalled();
+ expect(mockGetSession).toHaveBeenCalled();
+ });
+
+ it('sets user and token on successful session', async () => {
+ const mockUser = { id: '1', email: 'test@example.com' };
+ const mockToken = {
+ access_token: 'test-token',
+ expires_in: 3600,
+ user: mockUser
+ };
+ mockGetSession.mockResolvedValueOnce({
+ data: mockToken,
+ errors: null
+ });
+ mount(AuthorizerProvider, {
+ props: {
+ config: {
+ authorizerURL: 'http://localhost:8080',
+ redirectURL: 'http://localhost:3000',
+ clientID: 'test-client'
+ }
+ },
+ slots: {
+ default: () => h('div', 'test')
+ }
+ });
+ await flushPromises();
+ expect(globalContext.user).toEqual(mockUser);
+ expect(globalContext.token).toEqual(mockToken);
+ });
+
+ it('handles session errors gracefully', async () => {
+ mockGetSession.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'session expired' }]
+ });
+ mount(AuthorizerProvider, {
+ props: {
+ config: {
+ authorizerURL: 'http://localhost:8080',
+ redirectURL: 'http://localhost:3000',
+ clientID: 'test-client'
+ }
+ },
+ slots: {
+ default: () => h('div', 'test')
+ }
+ });
+ await flushPromises();
+ expect(globalContext.user).toBeNull();
+ expect(globalContext.token).toBeNull();
+ expect(globalContext.loading).toBe(false);
+ });
+});
diff --git a/src/__tests__/AuthorizerResetPassword.test.ts b/src/__tests__/AuthorizerResetPassword.test.ts
new file mode 100644
index 0000000..0c44920
--- /dev/null
+++ b/src/__tests__/AuthorizerResetPassword.test.ts
@@ -0,0 +1,83 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerResetPassword from '../components/AuthorizerResetPassword.vue';
+import globalConfig from '../state/globalConfig';
+
+const { mockResetPassword } = vi.hoisted(() => ({
+ mockResetPassword: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ resetPassword: mockResetPassword,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+vi.mock('../utils/url', () => ({
+ getSearchParams: () => ({ token: 'test-reset-token', redirect_uri: '' })
+}));
+
+describe('AuthorizerResetPassword', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalConfig, {
+ is_strong_password_enabled: false,
+ redirectURL: 'http://localhost:3000'
+ });
+ });
+
+ it('renders password and confirm password fields', () => {
+ const wrapper = mount(AuthorizerResetPassword);
+ expect(wrapper.find('#authorizer-reset-password').exists()).toBe(true);
+ expect(wrapper.find('#authorizer-reset-confirm-password').exists()).toBe(true);
+ });
+
+ it('validates password mismatch', async () => {
+ const wrapper = mount(AuthorizerResetPassword);
+ await wrapper.find('#authorizer-reset-password').setValue('password1');
+ await wrapper.find('#authorizer-reset-confirm-password').setValue('password2');
+ expect(wrapper.text()).toContain("Password and confirm passwords don't match");
+ });
+
+ it('calls resetPassword on submit', async () => {
+ mockResetPassword.mockResolvedValueOnce({
+ data: { message: 'Password reset successfully' },
+ errors: null
+ });
+
+ const onReset = vi.fn();
+ const wrapper = mount(AuthorizerResetPassword, { props: { onReset } });
+
+ await wrapper.find('#authorizer-reset-password').setValue('NewPass@123');
+ await wrapper.find('#authorizer-reset-confirm-password').setValue('NewPass@123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockResetPassword).toHaveBeenCalledWith(
+ expect.objectContaining({
+ token: 'test-reset-token',
+ password: 'NewPass@123',
+ confirm_password: 'NewPass@123'
+ })
+ );
+ expect(onReset).toHaveBeenCalled();
+ });
+
+ it('shows error on resetPassword failure', async () => {
+ mockResetPassword.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'Token expired' }]
+ });
+
+ const wrapper = mount(AuthorizerResetPassword);
+ await wrapper.find('#authorizer-reset-password').setValue('NewPass@123');
+ await wrapper.find('#authorizer-reset-confirm-password').setValue('NewPass@123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('Token expired');
+ });
+});
diff --git a/src/__tests__/AuthorizerSignup.test.ts b/src/__tests__/AuthorizerSignup.test.ts
new file mode 100644
index 0000000..65052c7
--- /dev/null
+++ b/src/__tests__/AuthorizerSignup.test.ts
@@ -0,0 +1,99 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerSignup from '../components/AuthorizerSignup.vue';
+import globalContext from '../state/globalContext';
+import globalConfig from '../state/globalConfig';
+
+const { mockSignup } = vi.hoisted(() => ({
+ mockSignup: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ signup: mockSignup,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerSignup', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalConfig, {
+ is_sign_up_enabled: true,
+ is_strong_password_enabled: false
+ });
+ globalContext.setAuthData = vi.fn();
+ });
+
+ it('renders email, password, and confirm password fields', () => {
+ const wrapper = mount(AuthorizerSignup);
+ expect(wrapper.find('#authorizer-sign-up-email').exists()).toBe(true);
+ expect(wrapper.find('#authorizer-sign-up-password').exists()).toBe(true);
+ expect(wrapper.find('#authorizer-sign-up-confirm-password').exists()).toBe(true);
+ });
+
+ it('validates password mismatch', async () => {
+ const wrapper = mount(AuthorizerSignup);
+ await wrapper.find('#authorizer-sign-up-password').setValue('password1');
+ await wrapper.find('#authorizer-sign-up-confirm-password').setValue('password2');
+ expect(wrapper.text()).toContain("Password and confirm passwords don't match");
+ });
+
+ it('calls signup on form submit with access_token response', async () => {
+ const mockUser = { id: '1', email: 'test@test.com' };
+ const mockToken = { access_token: 'tok', expires_in: 3600, user: mockUser };
+ mockSignup.mockResolvedValueOnce({ data: mockToken, errors: null });
+
+ const onSignup = vi.fn();
+ const wrapper = mount(AuthorizerSignup, { props: { onSignup } });
+
+ await wrapper.find('#authorizer-sign-up-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-sign-up-password').setValue('Test@123');
+ await wrapper.find('#authorizer-sign-up-confirm-password').setValue('Test@123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockSignup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ email: 'test@test.com',
+ password: 'Test@123',
+ confirm_password: 'Test@123'
+ })
+ );
+ expect(onSignup).toHaveBeenCalledWith(mockToken);
+ });
+
+ it('shows success message when signup returns message instead of token', async () => {
+ mockSignup.mockResolvedValueOnce({
+ data: { message: 'Please verify your email' },
+ errors: null
+ });
+
+ const wrapper = mount(AuthorizerSignup);
+ await wrapper.find('#authorizer-sign-up-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-sign-up-password').setValue('Test@123');
+ await wrapper.find('#authorizer-sign-up-confirm-password').setValue('Test@123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('Please verify your email');
+ });
+
+ it('handles signup errors', async () => {
+ mockSignup.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'Email already exists' }]
+ });
+
+ const wrapper = mount(AuthorizerSignup);
+ await wrapper.find('#authorizer-sign-up-email').setValue('test@test.com');
+ await wrapper.find('#authorizer-sign-up-password').setValue('Test@123');
+ await wrapper.find('#authorizer-sign-up-confirm-password').setValue('Test@123');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('Email already exists');
+ });
+});
diff --git a/src/__tests__/AuthorizerSocialLogin.test.ts b/src/__tests__/AuthorizerSocialLogin.test.ts
new file mode 100644
index 0000000..0adff30
--- /dev/null
+++ b/src/__tests__/AuthorizerSocialLogin.test.ts
@@ -0,0 +1,81 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount } from '@vue/test-utils';
+import AuthorizerSocialLogin from '../components/AuthorizerSocialLogin.vue';
+import globalConfig from '../state/globalConfig';
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerSocialLogin', () => {
+ beforeEach(() => {
+ Object.assign(globalConfig, {
+ authorizerURL: 'http://localhost:8080',
+ is_google_login_enabled: false,
+ is_github_login_enabled: false,
+ is_facebook_login_enabled: false,
+ is_linkedin_login_enabled: false,
+ is_apple_login_enabled: false,
+ is_twitter_login_enabled: false,
+ is_microsoft_login_enabled: false,
+ is_discord_login_enabled: false,
+ is_roblox_login_enabled: false,
+ is_basic_authentication_enabled: false,
+ is_magic_link_login_enabled: false
+ });
+ });
+
+ it('renders Google button when enabled', () => {
+ globalConfig.is_google_login_enabled = true;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).toContain('Sign in with Google');
+ });
+
+ it('renders Github button when enabled', () => {
+ globalConfig.is_github_login_enabled = true;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).toContain('Sign in with Github');
+ });
+
+ it('renders Facebook button when enabled', () => {
+ globalConfig.is_facebook_login_enabled = true;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).toContain('Sign in with Facebook');
+ });
+
+ it('hides disabled providers', () => {
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).not.toContain('Sign in with Google');
+ expect(wrapper.text()).not.toContain('Sign in with Github');
+ expect(wrapper.text()).not.toContain('Sign in with Facebook');
+ });
+
+ it('renders multiple providers when enabled', () => {
+ globalConfig.is_google_login_enabled = true;
+ globalConfig.is_github_login_enabled = true;
+ globalConfig.is_apple_login_enabled = true;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).toContain('Sign in with Google');
+ expect(wrapper.text()).toContain('Sign in with Github');
+ expect(wrapper.text()).toContain('Sign in with Apple');
+ });
+
+ it('shows separator when social login and basic auth are both enabled', () => {
+ globalConfig.is_google_login_enabled = true;
+ globalConfig.is_basic_authentication_enabled = true;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).toContain('OR');
+ });
+
+ it('hides separator when only social login is enabled', () => {
+ globalConfig.is_google_login_enabled = true;
+ globalConfig.is_basic_authentication_enabled = false;
+ globalConfig.is_magic_link_login_enabled = false;
+ const wrapper = mount(AuthorizerSocialLogin);
+ expect(wrapper.text()).not.toContain('OR');
+ });
+});
diff --git a/src/__tests__/AuthorizerVerifyOtp.test.ts b/src/__tests__/AuthorizerVerifyOtp.test.ts
new file mode 100644
index 0000000..326435b
--- /dev/null
+++ b/src/__tests__/AuthorizerVerifyOtp.test.ts
@@ -0,0 +1,108 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { mount, flushPromises } from '@vue/test-utils';
+import AuthorizerVerifyOtp from '../components/AuthorizerVerifyOtp.vue';
+import globalContext from '../state/globalContext';
+import globalConfig from '../state/globalConfig';
+
+const { mockVerifyOtp, mockResendOtp } = vi.hoisted(() => ({
+ mockVerifyOtp: vi.fn(),
+ mockResendOtp: vi.fn()
+}));
+
+vi.mock('@authorizerdev/authorizer-js', () => {
+ const MockAuthorizer = vi.fn().mockImplementation(() => ({
+ verifyOtp: mockVerifyOtp,
+ resendOtp: mockResendOtp,
+ getMetaData: vi.fn().mockResolvedValue({ data: {}, errors: null }),
+ getSession: vi.fn().mockResolvedValue({ data: null, errors: null })
+ }));
+ return { Authorizer: MockAuthorizer };
+});
+
+describe('AuthorizerVerifyOtp', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ Object.assign(globalConfig, {
+ is_sign_up_enabled: true
+ });
+ globalContext.setAuthData = vi.fn();
+ });
+
+ it('renders OTP input', () => {
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com' }
+ });
+ expect(wrapper.find('#authorizer-verify-otp').exists()).toBe(true);
+ });
+
+ it('validates empty OTP', async () => {
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com' }
+ });
+ await wrapper.find('#authorizer-verify-otp').setValue('');
+ expect(wrapper.text()).toContain('OTP is required');
+ });
+
+ it('validates invalid OTP format', async () => {
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com' }
+ });
+ await wrapper.find('#authorizer-verify-otp').setValue('abc');
+ expect(wrapper.text()).toContain('Please enter valid OTP');
+ });
+
+ it('calls verifyOtp on form submit', async () => {
+ const mockUser = { id: '1', email: 'test@test.com' };
+ const mockToken = { access_token: 'tok', expires_in: 3600, user: mockUser };
+ mockVerifyOtp.mockResolvedValueOnce({ data: mockToken, errors: null });
+
+ const onLogin = vi.fn();
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com', onLogin }
+ });
+
+ await wrapper.find('#authorizer-verify-otp').setValue('AB123C');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(mockVerifyOtp).toHaveBeenCalledWith(
+ expect.objectContaining({ email: 'test@test.com', otp: 'AB123C' })
+ );
+ expect(onLogin).toHaveBeenCalledWith(mockToken);
+ });
+
+ it('shows error on verifyOtp failure', async () => {
+ mockVerifyOtp.mockResolvedValueOnce({
+ data: null,
+ errors: [{ message: 'Invalid OTP' }]
+ });
+
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com' }
+ });
+
+ await wrapper.find('#authorizer-verify-otp').setValue('AB123C');
+ await wrapper.find('form').trigger('submit');
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('Invalid OTP');
+ });
+
+ it('calls resendOtp when resend link is clicked', async () => {
+ mockResendOtp.mockResolvedValueOnce({
+ data: { message: 'OTP sent successfully' },
+ errors: null
+ });
+
+ const wrapper = mount(AuthorizerVerifyOtp, {
+ props: { email: 'test@test.com', setView: vi.fn() }
+ });
+
+ const resendLink = wrapper.findAll('[class]').find((el) => el.text() === 'Resend OTP');
+ if (resendLink) {
+ await resendLink.trigger('click');
+ await flushPromises();
+ expect(mockResendOtp).toHaveBeenCalledWith({ email: 'test@test.com' });
+ }
+ });
+});
diff --git a/src/components/AuthorizerBasicAuthLogin.vue b/src/components/AuthorizerBasicAuthLogin.vue
index 3bce9a7..cb910c5 100644
--- a/src/components/AuthorizerBasicAuthLogin.vue
+++ b/src/components/AuthorizerBasicAuthLogin.vue
@@ -71,7 +71,7 @@
\ No newline at end of file
+
diff --git a/src/icons/Roblox.vue b/src/icons/Roblox.vue
index 7fda677..91a54dc 100644
--- a/src/icons/Roblox.vue
+++ b/src/icons/Roblox.vue
@@ -1,23 +1,27 @@
-
\ No newline at end of file
+
diff --git a/src/state/globalConfig.ts b/src/state/globalConfig.ts
index e195ea5..27c06e4 100644
--- a/src/state/globalConfig.ts
+++ b/src/state/globalConfig.ts
@@ -11,11 +11,15 @@ export default reactive({
is_apple_login_enabled: false,
is_discord_login_enabled: false,
is_roblox_login_enabled: false,
+ is_twitch_login_enabled: false,
is_email_verification_enabled: false,
is_basic_authentication_enabled: false,
is_magic_link_login_enabled: false,
is_sign_up_enabled: false,
is_strong_password_enabled: true,
is_twitter_login_enabled: false,
- is_microsoft_login_enabled: false
+ is_microsoft_login_enabled: false,
+ is_multi_factor_auth_enabled: false,
+ is_mobile_basic_authentication_enabled: false,
+ is_phone_verification_enabled: false
}) as AuthorizerConfig;
diff --git a/src/types/index.ts b/src/types/index.ts
index e96617c..43a1b1f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -15,17 +15,22 @@ export type AuthorizerConfig = {
is_microsoft_login_enabled: boolean;
is_discord_login_enabled: boolean;
is_roblox_login_enabled: boolean;
+ is_twitch_login_enabled: boolean;
is_email_verification_enabled: boolean;
is_basic_authentication_enabled: boolean;
is_magic_link_login_enabled: boolean;
is_sign_up_enabled: boolean;
is_strong_password_enabled: boolean;
+ is_multi_factor_auth_enabled: boolean;
+ is_mobile_basic_authentication_enabled: boolean;
+ is_phone_verification_enabled: boolean;
};
export type AuthorizerConfigInput = {
authorizerURL: string;
redirectURL?: string;
clientID?: string;
+ protocol?: 'graphql' | 'rest';
};
export type AuthorizerState = {
@@ -76,11 +81,15 @@ export type AuthorizerContextOutputType = {
is_microsoft_login_enabled: Ref;
is_discord_login_enabled: Ref;
is_roblox_login_enabled: Ref;
+ is_twitch_login_enabled: Ref;
is_email_verification_enabled: Ref;
is_basic_authentication_enabled: Ref;
is_magic_link_login_enabled: Ref;
is_sign_up_enabled: Ref;
is_strong_password_enabled: Ref;
+ is_multi_factor_auth_enabled: Ref;
+ is_mobile_basic_authentication_enabled: Ref;
+ is_phone_verification_enabled: Ref;
};
};