@@ -23,6 +23,9 @@ import {
2323 defaultConfig ,
2424 readConfig ,
2525 writeConfig ,
26+ customerConfigV2Path ,
27+ readCustomerConfigV2 ,
28+ writeCustomerConfigV2 ,
2629} from '../src/llmo-config.js' ;
2730
2831use ( sinonChai ) ;
@@ -200,4 +203,116 @@ describe('llmo-config utilities', () => {
200203 await expect ( writeConfig ( siteId , validConfig , s3Client ) ) . rejectedWith ( 'Failed to get version ID after writing LLMO config' ) ;
201204 } ) ;
202205 } ) ;
206+
207+ describe ( 'customerConfigV2Path' , ( ) => {
208+ it ( 'builds the V2 customer config path' , ( ) => {
209+ const organizationId = 'test-org-123' ;
210+ expect ( customerConfigV2Path ( organizationId ) ) . to . equals ( 'customer-config-v2/test-org-123/config.json' ) ;
211+ } ) ;
212+ } ) ;
213+
214+ describe ( 'readCustomerConfigV2' , ( ) => {
215+ const organizationId = 'test-org-456' ;
216+ const validCustomerConfig = { settings : { feature1 : true } , limits : { maxUsers : 100 } } ;
217+
218+ it ( 'retrieves and parses the customer config from S3' , async ( ) => {
219+ const body = {
220+ transformToString : sinon . stub ( ) . resolves ( JSON . stringify ( validCustomerConfig ) ) ,
221+ } ;
222+ s3Client . send . resolves ( { Body : body } ) ;
223+
224+ const result = await readCustomerConfigV2 ( organizationId , s3Client ) ;
225+
226+ expect ( result ) . deep . equals ( validCustomerConfig ) ;
227+ expect ( s3Client . send ) . calledOnce ;
228+ const command = s3Client . send . firstCall . args [ 0 ] ;
229+ expect ( command ) . instanceOf ( GetObjectCommand ) ;
230+ expect ( command . input . Bucket ) . equals ( 'default-test-bucket' ) ;
231+ expect ( command . input . Key ) . equals ( 'customer-config-v2/test-org-456/config.json' ) ;
232+ expect ( body . transformToString ) . calledOnce ;
233+ } ) ;
234+
235+ it ( 'uses provided bucket when options are set' , async ( ) => {
236+ const body = {
237+ transformToString : sinon . stub ( ) . resolves ( JSON . stringify ( validCustomerConfig ) ) ,
238+ } ;
239+ s3Client . send . resolves ( { Body : body } ) ;
240+
241+ await readCustomerConfigV2 ( organizationId , s3Client , {
242+ s3Bucket : 'custom-org-bucket' ,
243+ } ) ;
244+
245+ const command = s3Client . send . firstCall . args [ 0 ] ;
246+ expect ( command . input . Bucket ) . equals ( 'custom-org-bucket' ) ;
247+ } ) ;
248+
249+ it ( 'returns null when the config does not exist (NoSuchKey)' , async ( ) => {
250+ const error = new Error ( 'Missing key' ) ;
251+ error . name = 'NoSuchKey' ;
252+ s3Client . send . rejects ( error ) ;
253+
254+ const result = await readCustomerConfigV2 ( organizationId , s3Client ) ;
255+
256+ expect ( result ) . to . be . null ;
257+ } ) ;
258+
259+ it ( 'returns null when the config does not exist (NotFound)' , async ( ) => {
260+ const error = new Error ( 'Not found' ) ;
261+ error . name = 'NotFound' ;
262+ s3Client . send . rejects ( error ) ;
263+
264+ const result = await readCustomerConfigV2 ( organizationId , s3Client ) ;
265+
266+ expect ( result ) . to . be . null ;
267+ } ) ;
268+
269+ it ( 're-throws unexpected S3 errors' , async ( ) => {
270+ s3Client . send . rejects ( new Error ( 'S3 Service Error' ) ) ;
271+
272+ await expect ( readCustomerConfigV2 ( organizationId , s3Client ) ) . rejectedWith ( 'S3 Service Error' ) ;
273+ } ) ;
274+
275+ it ( 'throws when the S3 object body is missing' , async ( ) => {
276+ s3Client . send . resolves ( { } ) ;
277+
278+ await expect ( readCustomerConfigV2 ( organizationId , s3Client ) ) . rejectedWith ( 'Customer config V2 body is empty' ) ;
279+ } ) ;
280+
281+ it ( 'throws when the S3 object body cannot be parsed as JSON' , async ( ) => {
282+ const body = {
283+ transformToString : sinon . stub ( ) . resolves ( 'invalid json content' ) ,
284+ } ;
285+ s3Client . send . resolves ( { Body : body } ) ;
286+
287+ await expect ( readCustomerConfigV2 ( organizationId , s3Client ) ) . rejectedWith ( SyntaxError ) ;
288+ } ) ;
289+ } ) ;
290+
291+ describe ( 'writeCustomerConfigV2' , ( ) => {
292+ const organizationId = 'test-org-789' ;
293+ const customerConfig = { settings : { feature2 : false } , limits : { maxProjects : 50 } } ;
294+
295+ it ( 'writes the customer config to the default S3 bucket' , async ( ) => {
296+ s3Client . send . resolves ( { } ) ;
297+
298+ await writeCustomerConfigV2 ( organizationId , customerConfig , s3Client ) ;
299+
300+ expect ( s3Client . send ) . calledOnce ;
301+ const command = s3Client . send . firstCall . args [ 0 ] ;
302+ expect ( command ) . instanceOf ( PutObjectCommand ) ;
303+ expect ( command . input . Bucket ) . equals ( 'default-test-bucket' ) ;
304+ expect ( command . input . Key ) . equals ( 'customer-config-v2/test-org-789/config.json' ) ;
305+ expect ( command . input . Body ) . equals ( JSON . stringify ( customerConfig , null , 2 ) ) ;
306+ expect ( command . input . ContentType ) . equals ( 'application/json' ) ;
307+ } ) ;
308+
309+ it ( 'writes the customer config to a provided bucket' , async ( ) => {
310+ s3Client . send . resolves ( { } ) ;
311+
312+ await writeCustomerConfigV2 ( organizationId , customerConfig , s3Client , { s3Bucket : 'custom-org-bucket' } ) ;
313+
314+ const command = s3Client . send . firstCall . args [ 0 ] ;
315+ expect ( command . input . Bucket ) . equals ( 'custom-org-bucket' ) ;
316+ } ) ;
317+ } ) ;
203318} ) ;
0 commit comments