@accounter/server
Version:
Accounter GraphQL server
284 lines (239 loc) • 7.79 kB
text/typescript
import { createSchema, createYoga } from 'graphql-yoga';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { authPlugin, type RawAuth } from '../auth-plugin.js';
import { handleDevBypassAuth } from '../../modules/auth/providers/auth-context.provider.js';
vi.mock('../../modules/auth/providers/auth-context.provider.js', () => ({
handleDevBypassAuth: vi.fn(),
}));
describe('authPlugin', () => {
const originalAllowDevAuth = process.env.ALLOW_DEV_AUTH;
beforeEach(() => {
vi.clearAllMocks();
delete process.env.ALLOW_DEV_AUTH;
});
afterEach(() => {
if (originalAllowDevAuth === undefined) {
delete process.env.ALLOW_DEV_AUTH;
} else {
process.env.ALLOW_DEV_AUTH = originalAllowDevAuth;
}
});
const createTestYoga = () => {
const mockPool = {
query: vi.fn(),
};
return createYoga({
plugins: [authPlugin()],
schema: createSchema({
typeDefs: `type Query { test: String }`,
resolvers: {
Query: {
test: (_: unknown, __: unknown, context: { rawAuth: RawAuth, pool: typeof mockPool }) => {
return JSON.stringify(context.rawAuth);
},
},
},
}),
context: ({ request }) => ({
request,
pool: mockPool,
rawAuth: { authType: null, token: null } as RawAuth,
}),
});
};
it('should extract JWT token from Authorization header', async () => {
const yoga = createTestYoga();
const token = 'valid.jwt.token';
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(rawAuth).toEqual({
authType: 'jwt',
token,
});
});
it('should extract API key from X-API-Key header', async () => {
const yoga = createTestYoga();
const key = 'active-api-key';
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': key,
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(rawAuth).toEqual({
authType: 'apiKey',
token: key,
});
});
it('should prioritize JWT when both headers are present', async () => {
const yoga = createTestYoga();
const token = 'jwt.token.priority';
const key = 'ignored-api-key';
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'X-API-Key': key,
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(rawAuth).toEqual({
authType: 'jwt',
token,
});
});
it('should return null auth for missing headers', async () => {
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(rawAuth).toEqual({
authType: null,
token: null,
});
});
it('should iterate gracefully on malformed Authorization header', async () => {
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Basic somecreds',
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
// Should return null unless X-API-Key is also present (checked in logic)
expect(rawAuth).toEqual({
authType: null,
token: null,
});
});
it('should handle empty tokens as null', async () => {
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ', // Empty token part
'X-API-Key': ' ', // Whitespace only
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(rawAuth).toEqual({
authType: null,
token: null,
});
});
it('should ignore X-Dev-Auth when ALLOW_DEV_AUTH is missing', async () => {
vi.mocked(handleDevBypassAuth).mockResolvedValue({
authType: 'devBypass',
token: 'dev-user',
tenant: { businessId: 'biz-1' },
user: {
userId: 'dev-user',
email: 'dev-user',
roleId: 'business_owner',
permissions: [],
emailVerified: true,
permissionsVersion: 0,
},
});
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dev-Auth': 'dev-user',
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(handleDevBypassAuth).not.toHaveBeenCalled();
expect(rawAuth).toEqual({
authType: null,
token: null,
});
});
it('should ignore X-Dev-Auth when ALLOW_DEV_AUTH is false', async () => {
process.env.ALLOW_DEV_AUTH = '0';
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dev-Auth': 'dev-user',
'X-API-Key': 'fallback-key',
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(handleDevBypassAuth).not.toHaveBeenCalled();
expect(rawAuth).toEqual({
authType: 'apiKey',
token: 'fallback-key',
});
});
it('should call handleDevBypassAuth when ALLOW_DEV_AUTH=1 and X-Dev-Auth is present', async () => {
process.env.ALLOW_DEV_AUTH = '1';
vi.mocked(handleDevBypassAuth).mockResolvedValue({
authType: 'devBypass',
token: 'dev-user',
tenant: { businessId: 'biz-1' },
user: {
userId: 'dev-user',
email: 'dev-user',
roleId: 'business_owner',
permissions: [],
emailVerified: true,
permissionsVersion: 0,
},
});
const yoga = createTestYoga();
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dev-Auth': 'dev-user',
Authorization: 'Bearer ignored.jwt.token',
},
body: JSON.stringify({ query: '{ test }' }),
});
const result = await response.json();
const rawAuth = JSON.parse(result.data.test);
expect(handleDevBypassAuth).toHaveBeenCalledWith(
expect.objectContaining({ query: expect.any(Function) }),
'dev-user',
);
expect(rawAuth).toEqual({
authType: 'devBypass',
token: 'dev-user',
});
});
});