diff --git a/src/commands/components/actions.ts b/src/commands/components/actions.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/creds.test.ts b/src/creds.test.ts index 5d8585a2..9fd02187 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,48 @@ describe('creds', async () => { region: 'eu', }) }) + it('should throw an error if credentials file does not exist', async () => { + await expect(getCredentials('/temp/test/nonexistent.json')).rejects.toThrow( + new Error('The file requested was not found'), + ) + }) }) - 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('api.storyblok.com', '/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 15d47a27..c7a88c4b 100644 --- a/src/creds.ts +++ b/src/creds.ts @@ -39,261 +39,27 @@ export const addCredentials = async ({ 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`) } } -/* export interface NetrcMachine { - login: string - password: string - region: string -} - -export const getNetrcFilePath = () => { - const homeDirectory = process.env[ - process.platform.startsWith('win') ? 'USERPROFILE' : 'HOME' - ] || process.cwd() - - return join(homeDirectory, '.storyblok') -} - -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 removeCredentials = async (machineName: string, filepath: string = getStoryblokGlobalPath()) => { + const filePath = join(filepath, 'credentials.json') + const credentials = await getCredentials(filePath) -export const getNetrcCredentials = async (filePath: string = getNetrcFilePath()) => { - try { - await access(filePath) - } - catch { - return {} - } - try { - const content = await readNetrcFileAsync(filePath) + if (credentials[machineName]) { + delete credentials[machineName] - const machines = parseNetrcContent(content) - return machines - } - catch (error) { - 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(), - 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] = { - login, - password, - region, - } as NetrcMachine + await saveToFile(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 }) - // 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 - }) - - 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`) - } -} - -// 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) + konsola.ok(`Successfully removed entry for machine ${machineName} from ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) } - catch { - return + catch (error) { + throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error removing entry for machine ${machineName} from credentials.json file`) } - - if (machines[machineName]) { - // Remove the machine entry - delete machines[machineName] - // 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 - }) - - konsola.ok(`Successfully removed entry from ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) - } - } - 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 - } -} */ 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 1ddc0f76..890d377f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,5 +1,5 @@ // session.ts -import type { RegionCode } from './constants' +import { type RegionCode, regionsDomain } from './constants' import { addCredentials, getCredentials } from './creds' interface SessionState { @@ -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) { @@ -31,7 +31,7 @@ function createSession() { // If no environment variables, fall back to .storyblok/credentials.json const machines = await getCredentials() - const creds = machines[machineName || 'api.storyblok.com'] + const creds = machines[regionsDomain[region] || 'api.storyblok.com'] if (creds) { state.isLoggedIn = true state.login = creds.login 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'