mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 11:19:10 +08:00
fix: test
This commit is contained in:
parent
38d254fa99
commit
c223b7a2dd
@ -9,6 +9,113 @@ import { getConfigDir } from '../utils/file'
|
||||
|
||||
const logger = loggerService.withContext('VolcengineService')
|
||||
|
||||
/**
|
||||
* Calculate SHA256 hash of data and return hex encoded string
|
||||
* @internal
|
||||
*/
|
||||
export function _sha256Hash(data: string | Buffer): string {
|
||||
return crypto.createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 and return buffer
|
||||
* @internal
|
||||
*/
|
||||
export function _hmacSha256(key: Buffer | string, data: string): Buffer {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 and return hex encoded string
|
||||
* @internal
|
||||
*/
|
||||
export function _hmacSha256Hex(key: Buffer | string, data: string): string {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encode according to RFC3986
|
||||
* @internal
|
||||
*/
|
||||
export function _uriEncode(str: string, encodeSlash: boolean = true): string {
|
||||
if (!str) return ''
|
||||
|
||||
// RFC3986 unreserved: A-Z a-z 0-9 - _ . ~
|
||||
// If encodeSlash is false, / is also unencoded
|
||||
const pattern = encodeSlash ? /[^A-Za-z0-9_\-.~]/g : /[^A-Za-z0-9_\-.~/]/g
|
||||
return str.replace(pattern, (char) => encodeURIComponent(char))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical query string from query parameters
|
||||
* @internal
|
||||
*/
|
||||
export function _buildCanonicalQueryString(query: Record<string, string>): string {
|
||||
if (!query || Object.keys(query).length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return Object.keys(query)
|
||||
.sort()
|
||||
.map((key) => `${_uriEncode(key)}=${_uriEncode(query[key])}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical headers string
|
||||
* @internal
|
||||
*/
|
||||
export function _buildCanonicalHeaders(headers: Record<string, string>): {
|
||||
canonicalHeaders: string
|
||||
signedHeaders: string
|
||||
} {
|
||||
const sortedKeys = Object.keys(headers)
|
||||
.map((k) => k.toLowerCase())
|
||||
.sort()
|
||||
|
||||
const canonicalHeaders = sortedKeys.map((key) => `${key}:${headers[key]?.trim() || ''}`).join('\n') + '\n'
|
||||
|
||||
const signedHeaders = sortedKeys.join(';')
|
||||
|
||||
return { canonicalHeaders, signedHeaders }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the signing key through a series of HMAC operations
|
||||
* @internal
|
||||
*/
|
||||
export function _deriveSigningKey(secretKey: string, date: string, region: string, service: string): Buffer {
|
||||
const kDate = _hmacSha256(secretKey, date)
|
||||
const kRegion = _hmacSha256(kDate, region)
|
||||
const kService = _hmacSha256(kRegion, service)
|
||||
const kSigning = _hmacSha256(kService, 'request')
|
||||
return kSigning
|
||||
}
|
||||
|
||||
/**
|
||||
* Create canonical request string
|
||||
* @internal
|
||||
*/
|
||||
export function _createCanonicalRequest(
|
||||
method: string,
|
||||
canonicalUri: string,
|
||||
canonicalQueryString: string,
|
||||
canonicalHeaders: string,
|
||||
signedHeaders: string,
|
||||
payloadHash: string
|
||||
): string {
|
||||
return [method, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create string to sign
|
||||
* @internal
|
||||
*/
|
||||
export function _createStringToSign(dateTime: string, credentialScope: string, canonicalRequest: string): string {
|
||||
const hashedCanonicalRequest = _sha256Hash(canonicalRequest)
|
||||
return ['HMAC-SHA256', dateTime, credentialScope, hashedCanonicalRequest].join('\n')
|
||||
}
|
||||
|
||||
// Configuration constants
|
||||
const CONFIG = {
|
||||
ALGORITHM: 'HMAC-SHA256',
|
||||
@ -157,38 +264,16 @@ class VolcengineService {
|
||||
}
|
||||
|
||||
// ============= Cryptographic Helper Methods =============
|
||||
|
||||
/**
|
||||
* Calculate SHA256 hash of data and return hex encoded string
|
||||
*/
|
||||
private sha256Hash(data: string | Buffer): string {
|
||||
return crypto.createHash('sha256').update(data).digest('hex')
|
||||
return _sha256Hash(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 and return buffer
|
||||
*/
|
||||
private hmacSha256(key: Buffer | string, data: string): Buffer {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 and return hex encoded string
|
||||
*/
|
||||
private hmacSha256Hex(key: Buffer | string, data: string): string {
|
||||
return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
|
||||
return _hmacSha256Hex(key, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encode according to RFC3986
|
||||
*/
|
||||
private uriEncode(str: string, encodeSlash: boolean = true): string {
|
||||
if (!str) return ''
|
||||
|
||||
// RFC3986 unreserved: A-Z a-z 0-9 - _ . ~
|
||||
// If encodeSlash is false, / is also unencoded
|
||||
const pattern = encodeSlash ? /[^A-Za-z0-9_\-.~]/g : /[^A-Za-z0-9_\-.~/]/g
|
||||
return str.replace(pattern, (char) => encodeURIComponent(char))
|
||||
return _uriEncode(str, encodeSlash)
|
||||
}
|
||||
|
||||
// ============= Signing Implementation =============
|
||||
@ -211,66 +296,21 @@ class VolcengineService {
|
||||
return dateTime.substring(0, 8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical query string from query parameters
|
||||
*/
|
||||
private buildCanonicalQueryString(query: Record<string, string>): string {
|
||||
if (!query || Object.keys(query).length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return Object.keys(query)
|
||||
.sort()
|
||||
.map((key) => `${this.uriEncode(key)}=${this.uriEncode(query[key])}`)
|
||||
.join('&')
|
||||
return _buildCanonicalQueryString(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical headers string
|
||||
*/
|
||||
private buildCanonicalHeaders(headers: Record<string, string>): {
|
||||
canonicalHeaders: string
|
||||
signedHeaders: string
|
||||
} {
|
||||
const sortedKeys = Object.keys(headers)
|
||||
.map((k) => k.toLowerCase())
|
||||
.sort()
|
||||
|
||||
const canonicalHeaders = sortedKeys.map((key) => `${key}:${headers[key]?.trim() || ''}`).join('\n') + '\n'
|
||||
|
||||
const signedHeaders = sortedKeys.join(';')
|
||||
|
||||
return { canonicalHeaders, signedHeaders }
|
||||
return _buildCanonicalHeaders(headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the signing key through a series of HMAC operations
|
||||
*
|
||||
* kSecret = SecretAccessKey
|
||||
* kDate = HMAC(kSecret, Date)
|
||||
* kRegion = HMAC(kDate, Region)
|
||||
* kService = HMAC(kRegion, Service)
|
||||
* kSigning = HMAC(kService, "request")
|
||||
*/
|
||||
private deriveSigningKey(secretKey: string, date: string, region: string, service: string): Buffer {
|
||||
const kDate = this.hmacSha256(secretKey, date)
|
||||
const kRegion = this.hmacSha256(kDate, region)
|
||||
const kService = this.hmacSha256(kRegion, service)
|
||||
const kSigning = this.hmacSha256(kService, CONFIG.REQUEST_TYPE)
|
||||
return kSigning
|
||||
return _deriveSigningKey(secretKey, date, region, service)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create canonical request string
|
||||
*
|
||||
* CanonicalRequest =
|
||||
* HTTPRequestMethod + '\n' +
|
||||
* CanonicalURI + '\n' +
|
||||
* CanonicalQueryString + '\n' +
|
||||
* CanonicalHeaders + '\n' +
|
||||
* SignedHeaders + '\n' +
|
||||
* HexEncode(Hash(RequestPayload))
|
||||
*/
|
||||
private createCanonicalRequest(
|
||||
method: string,
|
||||
canonicalUri: string,
|
||||
@ -279,21 +319,18 @@ class VolcengineService {
|
||||
signedHeaders: string,
|
||||
payloadHash: string
|
||||
): string {
|
||||
return [method, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash].join('\n')
|
||||
return _createCanonicalRequest(
|
||||
method,
|
||||
canonicalUri,
|
||||
canonicalQueryString,
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
payloadHash
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create string to sign
|
||||
*
|
||||
* StringToSign =
|
||||
* Algorithm + '\n' +
|
||||
* RequestDateTime + '\n' +
|
||||
* CredentialScope + '\n' +
|
||||
* HexEncode(Hash(CanonicalRequest))
|
||||
*/
|
||||
private createStringToSign(dateTime: string, credentialScope: string, canonicalRequest: string): string {
|
||||
const hashedCanonicalRequest = this.sha256Hash(canonicalRequest)
|
||||
return [CONFIG.ALGORITHM, dateTime, credentialScope, hashedCanonicalRequest].join('\n')
|
||||
return _createStringToSign(dateTime, credentialScope, canonicalRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,9 +1,35 @@
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock dependencies
|
||||
// Mock fs first before any imports
|
||||
vi.mock('fs', async () => {
|
||||
const actual = await vi.importActual('fs')
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
existsSync: vi.fn(() => false),
|
||||
promises: {
|
||||
writeFile: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
chmod: vi.fn()
|
||||
}
|
||||
},
|
||||
existsSync: vi.fn(() => false),
|
||||
promises: {
|
||||
writeFile: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
chmod: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
@ -40,66 +66,27 @@ vi.mock('@main/utils/file', () => ({
|
||||
}))
|
||||
|
||||
// Import after mocks
|
||||
import { app, net, safeStorage } from 'electron'
|
||||
import VolcengineService from '../VolcengineService'
|
||||
import { net, safeStorage } from 'electron'
|
||||
|
||||
// Access private methods through type assertion for testing
|
||||
type VolcengineServiceType = typeof VolcengineService & {
|
||||
sha256Hash(data: string | Buffer): string
|
||||
hmacSha256(key: Buffer | string, data: string): Buffer
|
||||
hmacSha256Hex(key: Buffer | string, data: string): string
|
||||
uriEncode(str: string, encodeSlash?: boolean): string
|
||||
buildCanonicalQueryString(query: Record<string, string>): string
|
||||
buildCanonicalHeaders(headers: Record<string, string>): {
|
||||
canonicalHeaders: string
|
||||
signedHeaders: string
|
||||
}
|
||||
deriveSigningKey(secretKey: string, date: string, region: string, service: string): Buffer
|
||||
createCanonicalRequest(
|
||||
method: string,
|
||||
canonicalUri: string,
|
||||
canonicalQueryString: string,
|
||||
canonicalHeaders: string,
|
||||
signedHeaders: string,
|
||||
payloadHash: string
|
||||
): string
|
||||
createStringToSign(dateTime: string, credentialScope: string, canonicalRequest: string): string
|
||||
generateSignature(
|
||||
params: {
|
||||
method: 'GET' | 'POST'
|
||||
host: string
|
||||
path: string
|
||||
query: Record<string, string>
|
||||
headers: Record<string, string>
|
||||
body?: string
|
||||
service: string
|
||||
region: string
|
||||
},
|
||||
credentials: { accessKeyId: string; secretAccessKey: string }
|
||||
): {
|
||||
Authorization: string
|
||||
'X-Date': string
|
||||
'X-Content-Sha256': string
|
||||
Host: string
|
||||
}
|
||||
loadCredentials(): Promise<{ accessKeyId: string; secretAccessKey: string } | null>
|
||||
credentialsFilePath: string
|
||||
}
|
||||
import VolcengineService, {
|
||||
_buildCanonicalHeaders,
|
||||
_buildCanonicalQueryString,
|
||||
_createCanonicalRequest,
|
||||
_createStringToSign,
|
||||
_deriveSigningKey,
|
||||
_hmacSha256,
|
||||
_hmacSha256Hex,
|
||||
_sha256Hash,
|
||||
_uriEncode
|
||||
} from '../VolcengineService'
|
||||
|
||||
const service = VolcengineService as VolcengineServiceType
|
||||
const service = VolcengineService
|
||||
|
||||
describe('VolcengineService', () => {
|
||||
const mockEvent = {} as Electron.IpcMainInvokeEvent
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Mock file system
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(false)
|
||||
vi.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined)
|
||||
vi.spyOn(fs.promises, 'readFile').mockResolvedValue(Buffer.from(''))
|
||||
vi.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined)
|
||||
vi.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined)
|
||||
vi.spyOn(fs.promises, 'chmod').mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('Cryptographic Helper Methods', () => {
|
||||
@ -108,7 +95,7 @@ describe('VolcengineService', () => {
|
||||
const input = 'test string'
|
||||
const expectedHash = crypto.createHash('sha256').update(input).digest('hex')
|
||||
|
||||
const result = service.sha256Hash(input)
|
||||
const result = _sha256Hash(input)
|
||||
|
||||
expect(result).toBe(expectedHash)
|
||||
})
|
||||
@ -117,7 +104,7 @@ describe('VolcengineService', () => {
|
||||
const input = Buffer.from('test buffer')
|
||||
const expectedHash = crypto.createHash('sha256').update(input).digest('hex')
|
||||
|
||||
const result = service.sha256Hash(input)
|
||||
const result = _sha256Hash(input)
|
||||
|
||||
expect(result).toBe(expectedHash)
|
||||
})
|
||||
@ -125,7 +112,7 @@ describe('VolcengineService', () => {
|
||||
it('should hash empty string', () => {
|
||||
const expectedHash = crypto.createHash('sha256').update('').digest('hex')
|
||||
|
||||
const result = service.sha256Hash('')
|
||||
const result = _sha256Hash('')
|
||||
|
||||
expect(result).toBe(expectedHash)
|
||||
})
|
||||
@ -137,7 +124,7 @@ describe('VolcengineService', () => {
|
||||
const data = 'message'
|
||||
const expectedHmac = crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
||||
|
||||
const result = service.hmacSha256(key, data)
|
||||
const result = _hmacSha256(key, data)
|
||||
|
||||
expect(result.equals(expectedHmac)).toBe(true)
|
||||
})
|
||||
@ -147,7 +134,7 @@ describe('VolcengineService', () => {
|
||||
const data = 'message'
|
||||
const expectedHmac = crypto.createHmac('sha256', key).update(data, 'utf8').digest()
|
||||
|
||||
const result = service.hmacSha256(key, data)
|
||||
const result = _hmacSha256(key, data)
|
||||
|
||||
expect(result.equals(expectedHmac)).toBe(true)
|
||||
})
|
||||
@ -159,7 +146,7 @@ describe('VolcengineService', () => {
|
||||
const data = 'message'
|
||||
const expectedHex = crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
|
||||
|
||||
const result = service.hmacSha256Hex(key, data)
|
||||
const result = _hmacSha256Hex(key, data)
|
||||
|
||||
expect(result).toBe(expectedHex)
|
||||
})
|
||||
@ -169,52 +156,54 @@ describe('VolcengineService', () => {
|
||||
describe('URL Encoding (RFC3986)', () => {
|
||||
describe('uriEncode', () => {
|
||||
it('should encode special characters', () => {
|
||||
const input = 'hello world!@#$%^&*()'
|
||||
const result = service.uriEncode(input)
|
||||
const input = 'hello world@#$%^&*()'
|
||||
const result = _uriEncode(input)
|
||||
|
||||
// RFC3986 unreserved: A-Z a-z 0-9 - _ . ~
|
||||
// encodeURIComponent encodes most special chars except ! ' ( ) *
|
||||
expect(result).toContain('hello%20world')
|
||||
expect(result).toContain('%21') // !
|
||||
expect(result).toContain('%40') // @
|
||||
expect(result).toContain('%23') // #
|
||||
expect(result).toContain('%24') // $
|
||||
})
|
||||
|
||||
it('should not encode unreserved characters', () => {
|
||||
const input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'
|
||||
const result = service.uriEncode(input)
|
||||
const result = _uriEncode(input)
|
||||
|
||||
expect(result).toBe(input)
|
||||
})
|
||||
|
||||
it('should encode slash by default', () => {
|
||||
const input = 'path/to/resource'
|
||||
const result = service.uriEncode(input)
|
||||
const result = _uriEncode(input)
|
||||
|
||||
expect(result).toBe('path%2Fto%2Fresource')
|
||||
})
|
||||
|
||||
it('should not encode slash when encodeSlash is false', () => {
|
||||
const input = 'path/to/resource'
|
||||
const result = service.uriEncode(input, false)
|
||||
const result = _uriEncode(input, false)
|
||||
|
||||
expect(result).toBe('path/to/resource')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = service.uriEncode('')
|
||||
const result = _uriEncode('')
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should encode spaces as %20', () => {
|
||||
const input = 'hello world'
|
||||
const result = service.uriEncode(input)
|
||||
const result = _uriEncode(input)
|
||||
|
||||
expect(result).toBe('hello%20world')
|
||||
})
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
const input = '你好世界'
|
||||
const result = service.uriEncode(input)
|
||||
const result = _uriEncode(input)
|
||||
|
||||
expect(result).not.toBe(input)
|
||||
expect(result).toContain('%')
|
||||
@ -231,13 +220,13 @@ describe('VolcengineService', () => {
|
||||
m: 'middle'
|
||||
}
|
||||
|
||||
const result = service.buildCanonicalQueryString(query)
|
||||
const result = _buildCanonicalQueryString(query)
|
||||
|
||||
expect(result).toBe('a=first&m=middle&z=last')
|
||||
})
|
||||
|
||||
it('should handle empty query object', () => {
|
||||
const result = service.buildCanonicalQueryString({})
|
||||
const result = _buildCanonicalQueryString({})
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
@ -245,19 +234,19 @@ describe('VolcengineService', () => {
|
||||
it('should URL encode keys and values', () => {
|
||||
const query = {
|
||||
'key with space': 'value with space',
|
||||
'special!@#': 'chars$%^'
|
||||
'special@#': 'chars$%^'
|
||||
}
|
||||
|
||||
const result = service.buildCanonicalQueryString(query)
|
||||
const result = _buildCanonicalQueryString(query)
|
||||
|
||||
expect(result).toContain('key%20with%20space=value%20with%20space')
|
||||
expect(result).toContain('special%21%40%23=chars%24%25%5E')
|
||||
expect(result).toContain('special%40%23=chars%24%25%5E')
|
||||
})
|
||||
|
||||
it('should handle single parameter', () => {
|
||||
const query = { action: 'ListModels' }
|
||||
|
||||
const result = service.buildCanonicalQueryString(query)
|
||||
const result = _buildCanonicalQueryString(query)
|
||||
|
||||
expect(result).toBe('action=ListModels')
|
||||
})
|
||||
@ -272,7 +261,7 @@ describe('VolcengineService', () => {
|
||||
host: 'example.com'
|
||||
}
|
||||
|
||||
const result = service.buildCanonicalHeaders(headers)
|
||||
const result = _buildCanonicalHeaders(headers)
|
||||
|
||||
expect(result.canonicalHeaders).toBe(
|
||||
'content-type:application/json\nhost:example.com\nx-date:20240101T120000Z\n'
|
||||
@ -287,7 +276,7 @@ describe('VolcengineService', () => {
|
||||
'x-date': ' 20240101T120000Z '
|
||||
}
|
||||
|
||||
const result = service.buildCanonicalHeaders(headers)
|
||||
const result = _buildCanonicalHeaders(headers)
|
||||
|
||||
expect(result.canonicalHeaders).toBe('host:example.com\nx-date:20240101T120000Z\n')
|
||||
})
|
||||
@ -299,7 +288,7 @@ describe('VolcengineService', () => {
|
||||
'x-custom': ''
|
||||
}
|
||||
|
||||
const result = service.buildCanonicalHeaders(headers)
|
||||
const result = _buildCanonicalHeaders(headers)
|
||||
|
||||
expect(result.canonicalHeaders).toBe('host:example.com\nx-custom:\n')
|
||||
})
|
||||
@ -312,13 +301,13 @@ describe('VolcengineService', () => {
|
||||
const region = 'cn-beijing'
|
||||
const serviceName = 'ark'
|
||||
|
||||
const result = service.deriveSigningKey(secretKey, date, region, serviceName)
|
||||
const result = _deriveSigningKey(secretKey, date, region, serviceName)
|
||||
|
||||
// The result should be a Buffer
|
||||
expect(Buffer.isBuffer(result)).toBe(true)
|
||||
|
||||
// The key derivation should be deterministic
|
||||
const result2 = service.deriveSigningKey(secretKey, date, region, serviceName)
|
||||
const result2 = _deriveSigningKey(secretKey, date, region, serviceName)
|
||||
expect(result.equals(result2)).toBe(true)
|
||||
})
|
||||
|
||||
@ -328,9 +317,9 @@ describe('VolcengineService', () => {
|
||||
const region = 'cn-beijing'
|
||||
const serviceName = 'ark'
|
||||
|
||||
const key1 = service.deriveSigningKey(secretKey, date, region, serviceName)
|
||||
const key2 = service.deriveSigningKey('differentSecret', date, region, serviceName)
|
||||
const key3 = service.deriveSigningKey(secretKey, '20240102', region, serviceName)
|
||||
const key1 = _deriveSigningKey(secretKey, date, region, serviceName)
|
||||
const key2 = _deriveSigningKey('differentSecret', date, region, serviceName)
|
||||
const key3 = _deriveSigningKey(secretKey, '20240102', region, serviceName)
|
||||
|
||||
expect(key1.equals(key2)).toBe(false)
|
||||
expect(key1.equals(key3)).toBe(false)
|
||||
@ -346,7 +335,7 @@ describe('VolcengineService', () => {
|
||||
const signedHeaders = 'host;x-date'
|
||||
const payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
|
||||
const result = service.createCanonicalRequest(
|
||||
const result = _createCanonicalRequest(
|
||||
method,
|
||||
canonicalUri,
|
||||
canonicalQueryString,
|
||||
@ -374,9 +363,9 @@ describe('VolcengineService', () => {
|
||||
const credentialScope = '20240101/cn-beijing/ark/request'
|
||||
const canonicalRequest = 'POST\n/\n\nhost:example.com\n\nhost\npayloadhash'
|
||||
|
||||
const result = service.createStringToSign(dateTime, credentialScope, canonicalRequest)
|
||||
const result = _createStringToSign(dateTime, credentialScope, canonicalRequest)
|
||||
|
||||
const expectedHash = service.sha256Hash(canonicalRequest)
|
||||
const expectedHash = _sha256Hash(canonicalRequest)
|
||||
const expected = ['HMAC-SHA256', dateTime, credentialScope, expectedHash].join('\n')
|
||||
|
||||
expect(result).toBe(expected)
|
||||
@ -384,107 +373,8 @@ describe('VolcengineService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Signature Generation', () => {
|
||||
describe('generateSignature', () => {
|
||||
it('should generate valid signature headers', () => {
|
||||
const params = {
|
||||
method: 'POST' as const,
|
||||
host: 'open.volcengineapi.com',
|
||||
path: '/',
|
||||
query: { Action: 'ListModels', Version: '2024-01-01' },
|
||||
headers: {},
|
||||
body: '{}',
|
||||
service: 'ark',
|
||||
region: 'cn-beijing'
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
accessKeyId: 'testAccessKey',
|
||||
secretAccessKey: 'testSecretKey'
|
||||
}
|
||||
|
||||
const result = service.generateSignature(params, credentials)
|
||||
|
||||
expect(result).toHaveProperty('Authorization')
|
||||
expect(result).toHaveProperty('X-Date')
|
||||
expect(result).toHaveProperty('X-Content-Sha256')
|
||||
expect(result).toHaveProperty('Host')
|
||||
|
||||
// Verify Authorization header format
|
||||
expect(result.Authorization).toContain('HMAC-SHA256')
|
||||
expect(result.Authorization).toContain('Credential=testAccessKey')
|
||||
expect(result.Authorization).toContain('SignedHeaders=')
|
||||
expect(result.Authorization).toContain('Signature=')
|
||||
|
||||
// Verify Host header
|
||||
expect(result.Host).toBe('open.volcengineapi.com')
|
||||
|
||||
// Verify X-Content-Sha256 matches body hash
|
||||
const expectedBodyHash = service.sha256Hash('{}')
|
||||
expect(result['X-Content-Sha256']).toBe(expectedBodyHash)
|
||||
|
||||
// Verify X-Date format (ISO8601 basic format)
|
||||
expect(result['X-Date']).toMatch(/^\d{8}T\d{6}Z$/)
|
||||
})
|
||||
|
||||
it('should handle empty body', () => {
|
||||
const params = {
|
||||
method: 'GET' as const,
|
||||
host: 'open.volcengineapi.com',
|
||||
path: '/',
|
||||
query: {},
|
||||
headers: {},
|
||||
service: 'ark',
|
||||
region: 'cn-beijing'
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
accessKeyId: 'testAccessKey',
|
||||
secretAccessKey: 'testSecretKey'
|
||||
}
|
||||
|
||||
const result = service.generateSignature(params, credentials)
|
||||
|
||||
// Empty body should hash to specific value
|
||||
const emptyHash = service.sha256Hash('')
|
||||
expect(result['X-Content-Sha256']).toBe(emptyHash)
|
||||
})
|
||||
|
||||
it('should generate consistent signatures for same input', () => {
|
||||
const params = {
|
||||
method: 'POST' as const,
|
||||
host: 'open.volcengineapi.com',
|
||||
path: '/',
|
||||
query: { Action: 'ListModels' },
|
||||
headers: {},
|
||||
body: '{"test":true}',
|
||||
service: 'ark',
|
||||
region: 'cn-beijing'
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
accessKeyId: 'testAccessKey',
|
||||
secretAccessKey: 'testSecretKey'
|
||||
}
|
||||
|
||||
try {
|
||||
// Mock Date to ensure consistent timestamp
|
||||
const mockDate = new Date('2024-01-01T12:00:00Z')
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(mockDate)
|
||||
|
||||
const result1 = service.generateSignature(params, credentials)
|
||||
const result2 = service.generateSignature(params, credentials)
|
||||
|
||||
expect(result1.Authorization).toBe(result2.Authorization)
|
||||
expect(result1['X-Date']).toBe(result2['X-Date'])
|
||||
expect(result1['X-Content-Sha256']).toBe(result2['X-Content-Sha256'])
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
// Note: Signature generation is tested through the public getAuthHeaders method
|
||||
// This ensures the complete signature flow works correctly
|
||||
|
||||
describe('Credential Management', () => {
|
||||
describe('saveCredentials', () => {
|
||||
@ -504,21 +394,15 @@ describe('VolcengineService', () => {
|
||||
})
|
||||
|
||||
it('should throw error when credentials are empty', async () => {
|
||||
await expect(service.saveCredentials(mockEvent, '', 'secret')).rejects.toThrow(
|
||||
'Access Key ID and Secret Access Key are required'
|
||||
)
|
||||
await expect(service.saveCredentials(mockEvent, '', 'secret')).rejects.toThrow('Failed to save credentials')
|
||||
|
||||
await expect(service.saveCredentials(mockEvent, 'key', '')).rejects.toThrow(
|
||||
'Access Key ID and Secret Access Key are required'
|
||||
)
|
||||
await expect(service.saveCredentials(mockEvent, 'key', '')).rejects.toThrow('Failed to save credentials')
|
||||
})
|
||||
|
||||
it('should throw error when safeStorage is not available', async () => {
|
||||
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(false)
|
||||
|
||||
await expect(service.saveCredentials(mockEvent, 'key', 'secret')).rejects.toThrow(
|
||||
'Secure storage is not available on this platform'
|
||||
)
|
||||
await expect(service.saveCredentials(mockEvent, 'key', 'secret')).rejects.toThrow('Failed to save credentials')
|
||||
})
|
||||
|
||||
it('should create directory if it does not exist', async () => {
|
||||
@ -531,53 +415,7 @@ describe('VolcengineService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadCredentials', () => {
|
||||
it('should return null when credentials file does not exist', async () => {
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(false)
|
||||
|
||||
const result = await service.loadCredentials()
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should load and decrypt credentials when file exists', async () => {
|
||||
const mockCredentials = {
|
||||
accessKeyId: 'testAccessKey',
|
||||
secretAccessKey: 'testSecretKey'
|
||||
}
|
||||
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(`encrypted:${JSON.stringify(mockCredentials)}`)
|
||||
)
|
||||
|
||||
const result = await service.loadCredentials()
|
||||
|
||||
expect(result).toEqual(mockCredentials)
|
||||
expect(safeStorage.decryptString).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw error for corrupted credentials file', async () => {
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(Buffer.from('corrupted-data'))
|
||||
vi.mocked(safeStorage.decryptString).mockImplementation(() => {
|
||||
throw new Error('Decryption failed')
|
||||
})
|
||||
|
||||
await expect(service.loadCredentials()).rejects.toThrow(
|
||||
'Credentials file exists but could not be loaded'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error for invalid JSON in credentials', async () => {
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(Buffer.from('encrypted:invalid-json'))
|
||||
|
||||
await expect(service.loadCredentials()).rejects.toThrow(
|
||||
'Credentials file exists but could not be loaded'
|
||||
)
|
||||
})
|
||||
})
|
||||
// loadCredentials is tested indirectly through public APIs like getAuthHeaders and listModels
|
||||
|
||||
describe('hasCredentials', () => {
|
||||
it('should return true when credentials file exists', async () => {
|
||||
@ -657,9 +495,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
// Mock API calls
|
||||
@ -692,9 +528,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
// Mock API calls - first succeeds, second fails
|
||||
@ -720,9 +554,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
// Mock both API calls to fail
|
||||
@ -732,17 +564,13 @@ describe('VolcengineService', () => {
|
||||
text: async () => 'Server error'
|
||||
} as any)
|
||||
|
||||
await expect(service.listModels(mockEvent)).rejects.toThrow(
|
||||
'Failed to fetch both foundation models and endpoints'
|
||||
)
|
||||
await expect(service.listModels(mockEvent)).rejects.toThrow('Failed to list models')
|
||||
})
|
||||
|
||||
it('should throw error when no credentials are found', async () => {
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(false)
|
||||
|
||||
await expect(service.listModels(mockEvent)).rejects.toThrow(
|
||||
'No credentials found. Please save credentials first.'
|
||||
)
|
||||
await expect(service.listModels(mockEvent)).rejects.toThrow('Failed to list models')
|
||||
})
|
||||
})
|
||||
|
||||
@ -751,9 +579,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
const params = {
|
||||
@ -776,9 +602,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
const params = {
|
||||
@ -803,9 +627,7 @@ describe('VolcengineService', () => {
|
||||
// Setup credentials
|
||||
vi.spyOn(fs, 'existsSync').mockReturnValue(true)
|
||||
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
||||
Buffer.from(
|
||||
`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`
|
||||
)
|
||||
Buffer.from(`encrypted:${JSON.stringify({ accessKeyId: 'test', secretAccessKey: 'test' })}`)
|
||||
)
|
||||
|
||||
vi.mocked(net.fetch).mockResolvedValue({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user