Skip to content

Commit

Permalink
feat: refactor credential management to handle non-existent files and…
Browse files Browse the repository at this point in the history
… update session handling

- Updated `getCredentials` to create a credentials file if it does not exist, returning an empty object.
- Changed `removeCredentials` to accept a region code instead of a machine name, using the region to determine the machine name.
- Refactored `persistCredentials` to use region code directly.
- Updated tests to reflect changes in credential management and session handling.
  • Loading branch information
alvarosabu committed Jan 14, 2025
1 parent 3d2f936 commit 445f8ab
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 30 deletions.
8 changes: 4 additions & 4 deletions src/commands/login/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/commands/login/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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`)
}
Expand All @@ -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`)
}
Expand Down Expand Up @@ -124,7 +124,7 @@ export const loginCommand = program
else {
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)}`)
}
}
Expand Down
50 changes: 41 additions & 9 deletions src/commands/logout/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
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<string, any> | 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()
vi.clearAllMocks()
})

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()
})
})
10 changes: 6 additions & 4 deletions src/commands/logout/index.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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`)
}
Expand Down
9 changes: 4 additions & 5 deletions src/creds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ 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'),
)
it('should create a credentials.json file if it does not exist', async () => {
const credentials = await getCredentials('/temp/test/nonexistent.json')
expect(credentials).toEqual({})
})
})

Expand Down Expand Up @@ -69,7 +68,7 @@ describe('creds', async () => {
}),
}, '/temp')

await removeCredentials('api.storyblok.com', '/temp/test')
await removeCredentials('eu', '/temp/test')

const content = vol.readFileSync('/temp/test/credentials.json', 'utf8')
expect(content).toBe('{}')
Expand Down
16 changes: 14 additions & 2 deletions src/creds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { access } from 'node:fs/promises'
import { join } from 'node:path'
import { FileSystemError, handleFileSystemError, konsola } from './utils'
import chalk from 'chalk'
import { colorPalette } from './constants'
import type { RegionCode } from './constants'
import { colorPalette, regionsDomain } from './constants'
import { getStoryblokGlobalPath, readFile, saveToFile } from './utils/filesystem'

export const getCredentials = async (filePath = join(getStoryblokGlobalPath(), 'credentials.json')) => {
Expand All @@ -12,6 +13,11 @@ export const getCredentials = async (filePath = join(getStoryblokGlobalPath(), '
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 {}
}
Expand Down Expand Up @@ -43,9 +49,10 @@ export const addCredentials = async ({
}
}

export const removeCredentials = async (machineName: string, filepath: string = getStoryblokGlobalPath()) => {
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 (credentials[machineName]) {
delete credentials[machineName]
Expand All @@ -63,3 +70,8 @@ export const removeCredentials = async (machineName: string, filepath: string =
konsola.warn(`No entry found for machine ${machineName} in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true)
}
}

export const removeAllCredentials = async (filepath: string = getStoryblokGlobalPath()) => {
const filePath = join(filepath, 'credentials.json')
await saveToFile(filePath, JSON.stringify({}, null, 2), { mode: 0o600 })
}
4 changes: 2 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 addCredentials({
machineName,
machineName: regionsDomain[region] || 'api.storyblok.com',
login: state.login,
password: state.password,
region: state.region,
Expand Down

0 comments on commit 445f8ab

Please sign in to comment.