diff --git a/src/commands/login/index.test.ts b/src/commands/login/index.test.ts index be7fb574..d3e10e30 100644 --- a/src/commands/login/index.test.ts +++ b/src/commands/login/index.test.ts @@ -14,10 +14,10 @@ vi.mock('./actions', () => ({ })) vi.mock('../../creds', () => ({ - addNetrcEntry: vi.fn(), - isAuthorized: vi.fn(), - getNetrcCredentials: vi.fn(), - getCredentialsForMachine: vi.fn(), + getCredentials: vi.fn(), + addCredentials: vi.fn(), + removeCredentials: vi.fn(), + removeAllCredentials: vi.fn(), })) // Mocking the session module diff --git a/src/commands/login/index.ts b/src/commands/login/index.ts index c1edbd3b..2ffaeeaa 100644 --- a/src/commands/login/index.ts +++ b/src/commands/login/index.ts @@ -1,7 +1,7 @@ import chalk from 'chalk' import { input, password, select } from '@inquirer/prompts' import type { RegionCode } from '../../constants' -import { colorPalette, commands, regionNames, regions, regionsDomain } from '../../constants' +import { colorPalette, commands, regionNames, regions } from '../../constants' import { getProgram } from '../../program' import { CommandError, handleError, isRegion, konsola } from '../../utils' import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions' @@ -63,7 +63,7 @@ export const loginCommand = program try { const { user } = await loginWithToken(token, region) updateSession(user.email, token, region) - await persistCredentials(regionsDomain[region]) + await persistCredentials(region) konsola.ok(`Successfully logged in with token`) } @@ -85,7 +85,7 @@ export const loginCommand = program const { user } = await loginWithToken(userToken, region) updateSession(user.email, userToken, region) - await persistCredentials(regionsDomain[region]) + await persistCredentials(region) konsola.ok(`Successfully logged in with token`) } @@ -126,7 +126,7 @@ export const loginCommand = program else if (response?.access_token) { updateSession(userEmail, response.access_token, userRegion) } - await persistCredentials(regionsDomain[userRegion]) + await persistCredentials(region) konsola.ok(`Successfully logged in with email ${chalk.hex(colorPalette.PRIMARY)(userEmail)}`) } } diff --git a/src/commands/logout/index.test.ts b/src/commands/logout/index.test.ts index 54c01a91..3e439cbd 100644 --- a/src/commands/logout/index.test.ts +++ b/src/commands/logout/index.test.ts @@ -1,12 +1,39 @@ -import { isAuthorized, removeAllNetrcEntries } from '../../creds' import { logoutCommand } from './' +import { session } from '../../session' + +import { removeAllCredentials } from '../../creds' vi.mock('../../creds', () => ({ - isAuthorized: vi.fn(), - removeNetrcEntry: vi.fn(), - removeAllNetrcEntries: vi.fn(), + getCredentials: vi.fn(), + addCredentials: vi.fn(), + removeCredentials: vi.fn(), + removeAllCredentials: vi.fn(), })) +// Mocking the session module +vi.mock('../../session', () => { + let _cache: Record | null = null + const session = () => { + if (!_cache) { + _cache = { + state: { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + }, + updateSession: vi.fn(), + persistCredentials: vi.fn(), + initializeSession: vi.fn(), + } + } + return _cache + } + + return { + session, + } +}) + describe('logoutCommand', () => { beforeEach(() => { vi.resetAllMocks() @@ -14,15 +41,20 @@ describe('logoutCommand', () => { }) it('should log out the user if has previously login', async () => { - vi.mocked(isAuthorized).mockResolvedValue(true) - + session().state = { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + } await logoutCommand.parseAsync(['node', 'test']) - expect(removeAllNetrcEntries).toHaveBeenCalled() + expect(removeAllCredentials).toHaveBeenCalled() }) it('should not log out the user if has not previously login', async () => { - vi.mocked(isAuthorized).mockResolvedValue(false) + session().state = { + isLoggedIn: false, + } await logoutCommand.parseAsync(['node', 'test']) - expect(removeAllNetrcEntries).not.toHaveBeenCalled() + expect(removeAllCredentials).not.toHaveBeenCalled() }) }) diff --git a/src/commands/logout/index.ts b/src/commands/logout/index.ts index 5a63a72c..cfa57751 100644 --- a/src/commands/logout/index.ts +++ b/src/commands/logout/index.ts @@ -1,7 +1,8 @@ -import { isAuthorized, removeAllNetrcEntries } from '../../creds' +import { removeAllCredentials } from '../../creds' import { commands } from '../../constants' import { getProgram } from '../../program' import { handleError, konsola } from '../../utils' +import { session } from '../../session' const program = getProgram() // Get the shared singleton instance @@ -11,12 +12,13 @@ export const logoutCommand = program .action(async () => { const verbose = program.opts().verbose try { - const isAuth = await isAuthorized() - if (!isAuth) { + const { state, initializeSession } = session() + await initializeSession() + if (!state.isLoggedIn || !state.password || !state.region) { konsola.ok(`You are already logged out. If you want to login, please use the login command.`) return } - await removeAllNetrcEntries() + await removeAllCredentials() konsola.ok(`Successfully logged out`) } diff --git a/src/commands/user/index.ts b/src/commands/user/index.ts index a2dacda9..968b1b72 100644 --- a/src/commands/user/index.ts +++ b/src/commands/user/index.ts @@ -1,5 +1,4 @@ import chalk from 'chalk' -import type { NetrcMachine } from '../../creds' import { colorPalette, commands } from '../../constants' import { getProgram } from '../../program' import { CommandError, handleError, konsola } from '../../utils' @@ -21,7 +20,10 @@ export const userCommand = program return } try { - const { password, region } = state as NetrcMachine + const { password, region } = state + if (!password || !region) { + throw new Error('No password or region found') + } const { user } = await getUser(password, region) konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex(colorPalette.PRIMARY)(user.email)} on ${chalk.bold(region)} region`) } diff --git a/src/creds.test.ts b/src/creds.test.ts index 5d8585a2..3ec4c4b0 100644 --- a/src/creds.test.ts +++ b/src/creds.test.ts @@ -1,6 +1,5 @@ -import { addNetrcEntry, getNetrcCredentials, getNetrcFilePath, isAuthorized, removeAllNetrcEntries, removeNetrcEntry } from './creds' +import { addCredentials, getCredentials, removeCredentials } from './creds' import { vol } from 'memfs' -import { join } from 'pathe' // tell vitest to use fs mock from __mocks__ folder // this can be done in a setup file if fs should always be mocked vi.mock('node:fs') @@ -12,124 +11,19 @@ beforeEach(() => { }) describe('creds', async () => { - describe('getNetrcFilePath', async () => { - const originalPlatform = process.platform - const originalEnv = { ...process.env } - const originalCwd = process.cwd - - beforeEach(() => { - process.env = { ...originalEnv } - }) - - afterEach(() => { - // Restore the original platform after each test - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }) - // Restore process.cwd() - process.cwd = originalCwd - }) - - it('should return the correct path on Unix-like systems when HOME is set', () => { - // Mock the platform to be Unix-like - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - - // Set the HOME environment variable - process.env.HOME = '/home/testuser' - - const expectedPath = join('/home/testuser', '.netrc') - const result = getNetrcFilePath() - - expect(result).toBe(expectedPath) - }) - - it('should return the correct path on Windows systems when USERPROFILE is set', () => { - // Mock the platform to be Windows - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - - // Set the USERPROFILE environment variable - process.env.USERPROFILE = 'C:/Users/TestUser' - - const expectedPath = join('C:/Users/TestUser', '.netrc') - const result = getNetrcFilePath() - - expect(result).toBe(expectedPath) - }) - - it('should use process.cwd() when home directory is not set', () => { - // Mock the platform to be Unix-like - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - - // Remove HOME and USERPROFILE - delete process.env.HOME - delete process.env.USERPROFILE - - // Mock process.cwd() - process.cwd = vi.fn().mockReturnValue('/current/working/directory') - - const expectedPath = join('/current/working/directory', '.netrc') - const result = getNetrcFilePath() - - expect(result).toBe(expectedPath) - }) - - it('should use process.cwd() when HOME is empty', () => { - // Mock the platform to be Unix-like - Object.defineProperty(process, 'platform', { - value: 'linux', - }) - - // Set HOME to an empty string - process.env.HOME = '' - - // Mock process.cwd() - process.cwd = vi.fn().mockReturnValue('/current/working/directory') - - const expectedPath = join('/current/working/directory', '.netrc') - const result = getNetrcFilePath() - - expect(result).toBe(expectedPath) - }) - - it('should handle Windows platform when USERPROFILE is not set', () => { - // Mock the platform to be Windows - Object.defineProperty(process, 'platform', { - value: 'win32', - }) - - // Remove USERPROFILE - delete process.env.USERPROFILE - - // Mock process.cwd() - process.cwd = vi.fn().mockReturnValue('C:/Current/Directory') - - const expectedPath = join('C:/Current/Directory', '.netrc') - const result = getNetrcFilePath() - - expect(result).toBe(expectedPath) - }) - }) - - describe('getNetrcCredentials', () => { - it('should return empty object if .netrc file does not exist', async () => { - const creds = await getNetrcCredentials() - expect(creds).toEqual({}) - }) - it('should return the parsed content of .netrc file', async () => { + describe('getCredentials', () => { + it('should return the parsed content of credentials.json file', async () => { vol.fromJSON({ - 'test/.netrc': `machine api.storyblok.com - login julio.iglesias@storyblok.com - password my_access_token - region eu`, + 'test/credentials.json': JSON.stringify({ + 'api.storyblok.com': { + login: 'julio.iglesias@storyblok.com', + password: 'my_access_token', + region: 'eu', + }, + }), }, '/temp') - const credentials = await getNetrcCredentials('/temp/test/.netrc') + const credentials = await getCredentials('/temp/test/credentials.json') expect(credentials['api.storyblok.com']).toEqual({ login: 'julio.iglesias@storyblok.com', @@ -137,95 +31,47 @@ describe('creds', async () => { region: 'eu', }) }) + it('should create a credentials.json file if it does not exist', async () => { + const credentials = await getCredentials('/temp/test/nonexistent.json') + expect(credentials).toEqual({}) + }) }) - describe('addNetrcEntry', () => { - it('should add a new entry to an empty .netrc file', async () => { + describe('addCredentials', () => { + it('should add a new entry to an empty credentials file', async () => { vol.fromJSON({ - 'test/.netrc': '', + 'test/credentials.json': '{}', }, '/temp') - await addNetrcEntry({ - filePath: '/temp/test/.netrc', + await addCredentials({ + filePath: '/temp/test/credentials.json', machineName: 'api.storyblok.com', login: 'julio.iglesias@storyblok.com', password: 'my_access_token', region: 'eu', }) - const content = vol.readFileSync('/temp/test/.netrc', 'utf8') - - expect(content).toBe(`machine api.storyblok.com - login julio.iglesias@storyblok.com - password my_access_token - region eu -`) - }) - }) - - describe('removeNetrcEntry', () => { - it('should remove an entry from .netrc file', async () => { - vol.fromJSON({ - 'test/.netrc': `machine api.storyblok.com - login julio.iglesias@storyblok.com - password my_access_token - region eu`, - }, '/temp') - - await removeNetrcEntry('api.storyblok.com', '/temp/test/.netrc') - - const content = vol.readFileSync('/temp/test/.netrc', 'utf8') - - expect(content).toBe('') - }) - }) - - describe('removeAllNetrcEntries', () => { - it('should remove all entries from .netrc file', async () => { - vol.fromJSON({ - 'test/.netrc': `machine api.storyblok.com - login julio.iglesias@storyblok.com - password my_access_token - region eu`, - }, '/temp') - - await removeAllNetrcEntries('/temp/test/.netrc') - - const content = vol.readFileSync('/temp/test/.netrc', 'utf8') - - expect(content).toBe('') + const content = vol.readFileSync('/temp/test/credentials.json', 'utf8') + expect(content).toBe('{\n "api.storyblok.com": {\n "login": "julio.iglesias@storyblok.com",\n "password": "my_access_token",\n "region": "eu"\n }\n}') }) }) - describe('isAuthorized', () => { - beforeEach(() => { - vol.reset() - process.env.HOME = '/temp' // Ensure getNetrcFilePath points to /temp/.netrc + describe('removeCredentials', () => { + it('should remove an entry from credentials file', async () => { vol.fromJSON({ - '/temp/.netrc': `machine api.storyblok.com - login julio.iglesias@storyblok.com - password my_access_token - region eu`, - }) - }) - it('should return true if .netrc file contains an entry', async () => { - vi.doMock('./creds', () => { - return { - getNetrcCredentials: async () => { - return { - 'api.storyblok.com': { - login: 'julio.iglesias@storyblok.com', - password: 'my_access', - region: 'eu', - }, - } + 'test/credentials.json': JSON.stringify({ + 'api.storyblok.com': { + login: 'julio.iglesias@storyblok.com', + password: 'my_access_token', + region: 'eu', }, - } - }) + }), + }, '/temp') - const result = await isAuthorized() + await removeCredentials('eu', '/temp/test') - expect(result).toBe(true) + const content = vol.readFileSync('/temp/test/credentials.json', 'utf8') + expect(content).toBe('{}') }) }) }) diff --git a/src/creds.ts b/src/creds.ts index c39eba5e..bf0da728 100644 --- a/src/creds.ts +++ b/src/creds.ts @@ -1,261 +1,77 @@ -import { access, readFile, writeFile } from 'node:fs/promises' +import { access } from 'node:fs/promises' import { join } from 'node:path' import { FileSystemError, handleFileSystemError, konsola } from './utils' import chalk from 'chalk' -import { colorPalette, regionCodes } from './constants' import type { RegionCode } from './constants' +import { colorPalette, regionsDomain } from './constants' +import { getStoryblokGlobalPath, readFile, saveToFile } from './utils/filesystem' -export interface NetrcMachine { - login: string - password: string - region: RegionCode -} - -export const getNetrcFilePath = () => { - const homeDirectory = process.env[ - process.platform.startsWith('win') ? 'USERPROFILE' : 'HOME' - ] || process.cwd() - - return join(homeDirectory, '.netrc') -} - -const readNetrcFileAsync = async (filePath: string) => { - return await readFile(filePath, 'utf8') -} - -const preprocessNetrcContent = (content: string) => { - return content - .split('\n') - .map(line => line.split('#')[0].trim()) - .filter(line => line.length > 0) - .join(' ') -} - -const tokenizeNetrcContent = (content: string) => { - return content - .split(/\s+/) - .filter(token => token.length > 0) -} - -function includes(coll: ReadonlyArray, el: U): el is T { - return coll.includes(el as T) -} - -const parseNetrcTokens = (tokens: string[]) => { - const machines: Record = {} - let i = 0 - - while (i < tokens.length) { - const token = tokens[i] - - if (token === 'machine' || token === 'default') { - const machineName = token === 'default' ? 'default' : tokens[++i] - const machineData: Partial = {} - i++ - - while ( - i < tokens.length - && tokens[i] !== 'machine' - && tokens[i] !== 'default' - ) { - const key = tokens[i] - const value = tokens[++i] - if (key === 'region' && includes(regionCodes, value)) { - machineData[key] = value - } - else if (key === 'login' || key === 'password') { - machineData[key] = value - } - i++ - } - - machines[machineName] = machineData as NetrcMachine - } - else { - i++ - } - } - - return machines -} - -const parseNetrcContent = (content: string) => { - const preprocessedContent = preprocessNetrcContent(content) - const tokens = tokenizeNetrcContent(preprocessedContent) - return parseNetrcTokens(tokens) -} - -export const getNetrcCredentials = async (filePath: string = getNetrcFilePath()) => { +export const getCredentials = async (filePath = join(getStoryblokGlobalPath(), 'credentials.json')) => { try { await access(filePath) - } - catch { - return {} - } - try { - const content = await readNetrcFileAsync(filePath) - - const machines = parseNetrcContent(content) - return machines + const content = await readFile(filePath) + return JSON.parse(content) } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + // File doesn't exist, create it with empty credentials + await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 0o600 }) + return {} + } handleFileSystemError('read', error as NodeJS.ErrnoException) return {} } } -export const getCredentialsForMachine = ( - machines: Record = {}, - machineName?: string, -) => { - if (machineName) { - // Machine name provided - if (machines[machineName]) { - return machines[machineName] - } - else if (machines.default) { - return machines.default - } - else { - return null - } - } - else { - // No machine name provided - if (machines.default) { - return machines.default - } - else { - const machineNames = Object.keys(machines) - if (machineNames.length > 0) { - return machines[machineNames[0]] - } - else { - return null - } - } - } -} - -// Function to serialize machines object back into .netrc format -const serializeNetrcMachines = (machines: Record = {}) => { - let content = '' - for (const [machineName, properties] of Object.entries(machines)) { - content += `machine ${machineName}\n` - for (const [key, value] of Object.entries(properties)) { - content += ` ${key} ${value}\n` - } - } - return content -} - -// Function to add or update an entry in the .netrc file asynchronously -export const addNetrcEntry = async ({ - filePath = getNetrcFilePath(), +export const addCredentials = async ({ + filePath = join(getStoryblokGlobalPath(), 'credentials.json'), machineName, login, password, region, }: Record) => { - try { - let machines: Record = {} - - // Check if the file exists - try { - await access(filePath) - // File exists, read and parse it - const content = await readFile(filePath, 'utf8') - machines = parseNetrcContent(content) - } - catch { - // File does not exist - konsola.ok(`.netrc file not found at path: ${filePath}. A new file will be created.`) - } - - // Add or update the machine entry - machines[machineName] = { + const credentials = { + ...await getCredentials(filePath), + [machineName]: { login, password, region, - } as NetrcMachine - - // Serialize machines back into .netrc format - const newContent = serializeNetrcMachines(machines) + }, + } - // Write the updated content back to the .netrc file - await writeFile(filePath, newContent, { - mode: 0o600, // Set file permissions - }) + try { + await saveToFile(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 }) konsola.ok(`Successfully added/updated entry for machine ${machineName} in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) } catch (error) { - throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error adding/updating entry for machine ${machineName} in .netrc file`) + throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error adding/updating entry for machine ${machineName} in credentials.json file`) } } -// Function to remove an entry from the .netrc file asynchronously -export const removeNetrcEntry = async ( - machineName: string, - filePath = getNetrcFilePath(), -) => { - try { - let machines: Record = {} - - // Check if the file exists - try { - await access(filePath) - // File exists, read and parse it - const content = await readFile(filePath, 'utf8') - machines = parseNetrcContent(content) - } - catch { - return - } +export const removeCredentials = async (region: RegionCode, filepath: string = getStoryblokGlobalPath()) => { + const filePath = join(filepath, 'credentials.json') + const credentials = await getCredentials(filePath) + const machineName = regionsDomain[region] || 'api.storyblok.com' - if (machines[machineName]) { - // Remove the machine entry - delete machines[machineName] - // Serialize machines back into .netrc format - const newContent = serializeNetrcMachines(machines) + if (credentials[machineName]) { + delete credentials[machineName] - // Write the updated content back to the .netrc file - await writeFile(filePath, newContent, { - mode: 0o600, // Set file permissions - }) + try { + await saveToFile(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 }) - konsola.ok(`Successfully removed entry from ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) + konsola.ok(`Successfully removed entry for machine ${machineName} from ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) + } + catch (error) { + throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error removing entry for machine ${machineName} from credentials.json file`) } } - catch (error: unknown) { - handleFileSystemError('write', error as NodeJS.ErrnoException) - } -} - -export function removeAllNetrcEntries(filePath = getNetrcFilePath()) { - try { - return writeFile(filePath, '', { - mode: 0o600, // Set file permissions - }) - } - catch (error) { - handleFileSystemError('write', error as NodeJS.ErrnoException) + else { + konsola.warn(`No entry found for machine ${machineName} in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) } } -export async function isAuthorized() { - try { - const machines = await getNetrcCredentials() - // Check if there is any machine with a valid email and token - for (const machine of Object.values(machines)) { - if (machine.login && machine.password) { - return true - } - } - return false - } - catch (error: unknown) { - handleFileSystemError('authorization_check', error as NodeJS.ErrnoException) - return false - } +export const removeAllCredentials = async (filepath: string = getStoryblokGlobalPath()) => { + const filePath = join(filepath, 'credentials.json') + await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 0o600 }) } diff --git a/src/index.ts b/src/index.ts index 5c92d310..4cdfce6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,6 @@ import './commands/logout' import './commands/user' import './commands/pull-languages' -import { customFetch } from './utils/fetch' -import { getStoryblokUrl } from './utils/api-routes' import { session } from './session' dotenv.config() // This will load variables from .env into process.env @@ -35,18 +33,10 @@ program.command('test').action(async () => { try { const { state, initializeSession } = session() await initializeSession() - const url = getStoryblokUrl() if (!state.password) { throw new Error('No password found') } - - const response = await customFetch(`${url}/spaces/2950170505/components`, { - headers: { - Authorization: state.password, - }, - }) - console.log(response) } catch (error) { handleError(error as Error, verbose) diff --git a/src/session.test.ts b/src/session.test.ts index 5dbb0839..28ec7759 100644 --- a/src/session.test.ts +++ b/src/session.test.ts @@ -1,27 +1,28 @@ // session.test.ts import { session } from './session' -import { getCredentialsForMachine } from './creds' +import { getCredentials } from './creds' import type { Mock } from 'vitest' vi.mock('./creds', () => ({ - getNetrcCredentials: vi.fn(), - getCredentialsForMachine: vi.fn(), + getCredentials: vi.fn(), })) -const mockedGetCredentialsForMachine = getCredentialsForMachine as Mock +const mockedGetCredentials = getCredentials as Mock describe('session', () => { beforeEach(() => { vi.resetAllMocks() vi.clearAllMocks() }) - describe('session initialization with netrc', () => { - it('should initialize session with netrc credentials', async () => { - mockedGetCredentialsForMachine.mockReturnValue({ - login: 'test_login', - password: 'test_token', - region: 'test_region', + describe('session initialization with json', () => { + it('should initialize session with json credentials', async () => { + mockedGetCredentials.mockReturnValue({ + 'api.storyblok.com': { + login: 'test_login', + password: 'test_token', + region: 'test_region', + }, }) const userSession = session() await userSession.initializeSession() @@ -30,56 +31,6 @@ describe('session', () => { expect(userSession.state.password).toBe('test_token') expect(userSession.state.region).toBe('test_region') }) - it('should initialize session with netrc credentials for a specific machine', async () => { - mockedGetCredentialsForMachine.mockReturnValue({ - login: 'test_login', - password: 'test_token', - region: 'test_region', - }) - const userSession = session() - await userSession.initializeSession('test-machine') - expect(userSession.state.isLoggedIn).toBe(true) - expect(userSession.state.login).toBe('test_login') - expect(userSession.state.password).toBe('test_token') - expect(userSession.state.region).toBe('test_region') - }) - - it('should initialize session with netrc credentials for a specific machine when no matching machine is present', async () => { - mockedGetCredentialsForMachine.mockReturnValue(undefined) - const userSession = session() - await userSession.initializeSession('nonexistent-machine') - expect(userSession.state.isLoggedIn).toBe(false) - expect(userSession.state.login).toBe(undefined) - expect(userSession.state.password).toBe(undefined) - expect(userSession.state.region).toBe(undefined) - }) - /* - it('should initialize session with netrc credentials for a specific machine', async () => { - const userSession = session() - await userSession.initializeSession('test-machine') - expect(userSession.state.isLoggedIn).toBe(true) - expect(userSession.state.login).toBe('test_login') - expect(userSession.state.password).toBe('test_token') - expect(userSession.state.region).toBe('test_region') - }) - - it('should initialize session with netrc credentials for a specific machine when multiple machines are present', async () => { - const userSession = session() - await userSession.initializeSession('test-machine-2') - expect(userSession.state.isLoggedIn).toBe(true) - expect(userSession.state.login).toBe('test_login_2') - expect(userSession.state.password).toBe('test_token_2') - expect(userSession.state.region).toBe('test_region_2') - }) - - it('should initialize session with netrc credentials for a specific machine when no matching machine is present', async () => { - const userSession = session() - await userSession.initializeSession('nonexistent-machine') - expect(userSession.state.isLoggedIn).toBe(false) - expect(userSession.state.login).toBe(undefined) - expect(userSession.state.password).toBe(undefined) - expect(userSession.state.region).toBe(undefined) - }) */ }) describe('session initialization with environment variables', () => { beforeEach(() => { diff --git a/src/session.ts b/src/session.ts index 8107f933..432fa707 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,6 @@ // session.ts -import type { RegionCode } from './constants' -import { addNetrcEntry, getCredentialsForMachine, getNetrcCredentials } from './creds' +import { type RegionCode, regionsDomain } from './constants' +import { addCredentials, getCredentials } from './creds' interface SessionState { isLoggedIn: boolean @@ -17,7 +17,7 @@ function createSession() { isLoggedIn: false, } - async function initializeSession(machineName?: string) { + async function initializeSession(region = 'eu' as RegionCode) { // First, check for environment variables const envCredentials = getEnvCredentials() if (envCredentials) { @@ -29,9 +29,9 @@ function createSession() { return } - // If no environment variables, fall back to netrc - const machines = await getNetrcCredentials() - const creds = getCredentialsForMachine(machines, machineName) + // If no environment variables, fall back to .storyblok/credentials.json + const machines = await getCredentials() + const creds = machines[regionsDomain[region] || 'api.storyblok.com'] if (creds) { state.isLoggedIn = true state.login = creds.login @@ -63,10 +63,10 @@ function createSession() { return null } - async function persistCredentials(machineName: string) { + async function persistCredentials(region: RegionCode) { if (state.isLoggedIn && state.login && state.password && state.region) { - await addNetrcEntry({ - machineName, + await addCredentials({ + machineName: regionsDomain[region] || 'api.storyblok.com', login: state.login, password: state.password, region: state.region, diff --git a/src/utils/filesystem.test.ts b/src/utils/filesystem.test.ts index 5b074e9a..500862a2 100644 --- a/src/utils/filesystem.test.ts +++ b/src/utils/filesystem.test.ts @@ -1,6 +1,6 @@ import { vol } from 'memfs' -import { resolvePath, saveToFile } from './filesystem' -import { resolve } from 'node:path' +import { getStoryblokGlobalPath, resolvePath, saveToFile } from './filesystem' +import { join, resolve } from 'node:path' // tell vitest to use fs mock from __mocks__ folder // this can be done in a setup file if fs should always be mocked @@ -14,6 +14,108 @@ beforeEach(() => { }) describe('filesystem utils', async () => { + describe('getStoryblokGlobalPath', async () => { + const originalPlatform = process.platform + const originalEnv = { ...process.env } + const originalCwd = process.cwd + + beforeEach(() => { + process.env = { ...originalEnv } + }) + + afterEach(() => { + // Restore the original platform after each test + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }) + // Restore process.cwd() + process.cwd = originalCwd + }) + it('should return the correct path on Unix-like systems when HOME is set', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Set the HOME environment variable + process.env.HOME = '/home/testuser' + + const expectedPath = join('/home/testuser', '.storyblok') + const result = getStoryblokGlobalPath() + + expect(result).toBe(expectedPath) + }) + + it('should return the correct path on Windows systems when USERPROFILE is set', () => { + // Mock the platform to be Windows + Object.defineProperty(process, 'platform', { + value: 'win32', + }) + + // Set the USERPROFILE environment variable + process.env.USERPROFILE = 'C:/Users/TestUser' + + const expectedPath = join('C:/Users/TestUser', '.storyblok') + const result = getStoryblokGlobalPath() + + expect(result).toBe(expectedPath) + }) + + it('should use process.cwd() when home directory is not set', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Remove HOME and USERPROFILE + delete process.env.HOME + delete process.env.USERPROFILE + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('/current/working/directory') + + const expectedPath = join('/current/working/directory', '.storyblok') + const result = getStoryblokGlobalPath() + + expect(result).toBe(expectedPath) + }) + + it('should use process.cwd() when HOME is empty', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Set HOME to an empty string + process.env.HOME = '' + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('/current/working/directory') + + const expectedPath = join('/current/working/directory', '.storyblok') + const result = getStoryblokGlobalPath() + + expect(result).toBe(expectedPath) + }) + + it('should handle Windows platform when USERPROFILE is not set', () => { + // Mock the platform to be Windows + Object.defineProperty(process, 'platform', { + value: 'win32', + }) + + // Remove USERPROFILE + delete process.env.USERPROFILE + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('C:/Current/Directory') + + const expectedPath = join('C:/Current/Directory', '.storyblok') + const result = getStoryblokGlobalPath() + + expect(result).toBe(expectedPath) + }) + }) describe('saveToFile', async () => { it('should save the data to the file', async () => { const filePath = '/path/to/file.txt' diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts index 5e9eaa03..2372973f 100644 --- a/src/utils/filesystem.ts +++ b/src/utils/filesystem.ts @@ -1,8 +1,20 @@ -import { parse, resolve } from 'node:path' -import { mkdir, writeFile } from 'node:fs/promises' +import { join, parse, resolve } from 'node:path' +import { mkdir, readFile as readFileImpl, writeFile } from 'node:fs/promises' import { handleFileSystemError } from './error/filesystem-error' -export const saveToFile = async (filePath: string, data: string) => { +export interface FileOptions { + mode?: number +} + +export const getStoryblokGlobalPath = () => { + const homeDirectory = process.env[ + process.platform.startsWith('win') ? 'USERPROFILE' : 'HOME' + ] || process.cwd() + + return join(homeDirectory, '.storyblok') +} + +export const saveToFile = async (filePath: string, data: string, options: FileOptions) => { // Get the directory path const resolvedPath = parse(filePath).dir @@ -17,11 +29,21 @@ export const saveToFile = async (filePath: string, data: string) => { // Write the file try { - await writeFile(filePath, data) + await writeFile(filePath, data, options) } catch (writeError) { handleFileSystemError('write', writeError as Error) } } +export const readFile = async (filePath: string) => { + try { + return await readFileImpl(filePath, 'utf8') + } + catch (error) { + handleFileSystemError('read', error as Error) + return '' + } +} + export const resolvePath = (path: string | undefined, folder: string) => path ? resolve(process.cwd(), path) : resolve(resolve(process.cwd(), '.storyblok'), folder)