Skip to content

Commit 97a707e

Browse files
committed
feat: Add environment variable rollout details API and CLI support
This commit introduces the necessary API integration and CLI command enhancements to support retrieving environment variable rollout details.
1 parent b382cd1 commit 97a707e

File tree

4 files changed

+227
-5
lines changed

4 files changed

+227
-5
lines changed

src/cmd/api.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,23 @@ module.exports = class ParticleApi {
480480
}));
481481
}
482482

483+
getRollout({ org, productId, deviceId }) {
484+
let basePath;
485+
if (org) {
486+
basePath = `/v1/orgs/${org}/products/${productId}/env-vars`;
487+
} else if (productId) {
488+
basePath = `/v1/products/${productId}/env-vars`;
489+
} else {
490+
basePath = `/v1/env-vars`;
491+
}
492+
const uri = `${basePath}${deviceId ? `/${deviceId}` : ''}/rollout`;
493+
return this._wrap(this.api.request({
494+
uri,
495+
method: 'get',
496+
auth: this.accessToken
497+
}));
498+
}
499+
483500
_wrap(promise){
484501
return Promise.resolve(promise)
485502
.then(result => result.body || result)

src/cmd/api.test.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
'use strict';
2+
const { expect } = require('../../test/setup');
3+
const sinon = require('sinon');
4+
const ParticleApi = require('./api');
5+
6+
describe('ParticleApi', () => {
7+
let particleApi;
8+
let sandbox;
9+
10+
beforeEach(() => {
11+
sandbox = sinon.createSandbox();
12+
particleApi = new ParticleApi('test-base-url', { accessToken: 'test-token' });
13+
});
14+
15+
afterEach(() => {
16+
sandbox.restore();
17+
});
18+
19+
describe('getRollout', () => {
20+
it('should call the correct API endpoint for product rollout without org', async () => {
21+
const productId = 'testProductId';
22+
const expectedUri = `/v1/products/${productId}/env-vars/rollout`;
23+
const expectedResponse = { body: { some: 'data' } };
24+
25+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
26+
27+
const result = await particleApi.getRollout({ productId });
28+
29+
expect(requestStub).to.have.been.calledWithMatch({
30+
uri: expectedUri,
31+
method: 'get',
32+
auth: 'test-token'
33+
});
34+
expect(result).to.deep.equal(expectedResponse.body);
35+
});
36+
37+
it('should call the correct API endpoint for product rollout with org', async () => {
38+
const org = 'testOrg';
39+
const productId = 'testProductId';
40+
const expectedUri = `/v1/orgs/${org}/products/${productId}/env-vars/rollout`;
41+
const expectedResponse = { body: { some: 'other-data' } };
42+
43+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
44+
45+
const result = await particleApi.getRollout({ org, productId });
46+
47+
expect(requestStub).to.have.been.calledWithMatch({
48+
uri: expectedUri,
49+
method: 'get',
50+
auth: 'test-token'
51+
});
52+
expect(result).to.deep.equal(expectedResponse.body);
53+
});
54+
55+
it('should call the correct API endpoint for sandbox rollout when no org or product is provided', async () => {
56+
const expectedUri = `/v1/env-vars/rollout`;
57+
const expectedResponse = { body: { some: 'sandbox-data' } };
58+
59+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
60+
61+
const result = await particleApi.getRollout({});
62+
63+
expect(requestStub).to.have.been.calledWithMatch({
64+
uri: expectedUri,
65+
method: 'get',
66+
auth: 'test-token'
67+
});
68+
expect(result).to.deep.equal(expectedResponse.body);
69+
});
70+
71+
it('should call the correct API endpoint for product rollout with deviceId', async () => {
72+
const productId = 'testProductId';
73+
const deviceId = 'testDeviceId';
74+
const expectedUri = `/v1/products/${productId}/env-vars/${deviceId}/rollout`;
75+
const expectedResponse = { body: { some: 'device-data' } };
76+
77+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
78+
79+
const result = await particleApi.getRollout({ productId, deviceId });
80+
81+
expect(requestStub).to.have.been.calledWithMatch({
82+
uri: expectedUri,
83+
method: 'get',
84+
auth: 'test-token'
85+
});
86+
expect(result).to.deep.equal(expectedResponse.body);
87+
});
88+
89+
it('should call the correct API endpoint for product rollout with org and deviceId', async () => {
90+
const org = 'testOrg';
91+
const productId = 'testProductId';
92+
const deviceId = 'testDeviceId';
93+
const expectedUri = `/v1/orgs/${org}/products/${productId}/env-vars/${deviceId}/rollout`;
94+
const expectedResponse = { body: { some: 'org-device-data' } };
95+
96+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
97+
98+
const result = await particleApi.getRollout({ org, productId, deviceId });
99+
100+
expect(requestStub).to.have.been.calledWithMatch({
101+
uri: expectedUri,
102+
method: 'get',
103+
auth: 'test-token'
104+
});
105+
expect(result).to.deep.equal(expectedResponse.body);
106+
});
107+
108+
it('should call the correct API endpoint for sandbox rollout with deviceId', async () => {
109+
const deviceId = 'testDeviceId';
110+
const expectedUri = `/v1/env-vars/${deviceId}/rollout`;
111+
const expectedResponse = { body: { some: 'sandbox-device-data' } };
112+
113+
const requestStub = sandbox.stub(particleApi.api, 'request').resolves(expectedResponse);
114+
115+
const result = await particleApi.getRollout({ deviceId });
116+
117+
expect(requestStub).to.have.been.calledWithMatch({
118+
uri: expectedUri,
119+
method: 'get',
120+
auth: 'test-token'
121+
});
122+
expect(result).to.deep.equal(expectedResponse.body);
123+
});
124+
125+
it('should handle API errors', async () => {
126+
const productId = 'testProductId';
127+
const expectedError = new Error('API Error');
128+
expectedError.statusCode = 401;
129+
expectedError.body = { error_description: 'Unauthorized' };
130+
131+
sandbox.stub(particleApi.api, 'request').rejects(expectedError);
132+
133+
try {
134+
await particleApi.getRollout({ productId });
135+
expect.fail('should have thrown an error');
136+
} catch (error) {
137+
expect(error.message).to.equal('Unauthorized');
138+
expect(error.name).to.equal('UnauthorizedError');
139+
}
140+
});
141+
});
142+
});

src/cmd/env-vars.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,28 @@ module.exports = class EnvVarsCommand extends CLICommandBase {
155155
};
156156
}
157157

158-
async rollout() {
159-
this.ui.write('the rollout command is not implemented yet');
158+
async rollout({ org, product, file }) {
159+
const rolloutData = await this.ui.showBusySpinnerUntilResolved('Getting rollout information...',this.api.getRollout({ org, product }));
160+
if (file) {
161+
await fs.writeFile(file, JSON.stringify(rolloutData, null, 2));
162+
this.ui.write(`Rollout information has been saved to ${file}`);
163+
} else {
164+
this._displayRollout(rolloutData);
165+
}
166+
}
167+
168+
_displayRollout(rolloutData) {
169+
const { percentage, device_ids, env } = rolloutData;
170+
this.ui.write(this.ui.chalk.bold('Rollout Details:'));
171+
this.ui.write(` Rollout Percentage: ${percentage}%`);
172+
this.ui.write(' Devices in Rollout:');
173+
device_ids.forEach(deviceId => {
174+
this.ui.write(` - ${deviceId}`);
175+
});
176+
this.ui.write(' Environment Variables:');
177+
Object.entries(env).forEach(([key, value]) => {
178+
this.ui.write(` ${key}: ${value}`);
179+
});
160180
}
161181
};
162182

src/cmd/env-vars.test.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,52 @@ describe('Env Vars Command', () => {
366366
});
367367

368368
describe('rollout', () => {
369-
it('shows a not implemented message', async () => {
370-
await envVarsCommands.rollout({});
371-
expect(envVarsCommands.ui.write).to.have.been.calledWith('the rollout command is not implemented yet');
369+
it('should display rollout information', async () => {
370+
const rolloutData = {
371+
percentage: 50,
372+
device_ids: ['device1', 'device2'],
373+
env: {
374+
FOO: 'bar',
375+
BAZ: 'qux'
376+
}
377+
};
378+
sinon.stub(envVarsCommands.api, 'getRollout').resolves(rolloutData);
379+
380+
await envVarsCommands.rollout({ product: 'my-product' });
381+
382+
expect(envVarsCommands.ui.showBusySpinnerUntilResolved).calledWith('Getting rollout information...');
383+
const writeCalls = envVarsCommands.ui.write.getCalls().map(c => c.args[0]);
384+
385+
expect(writeCalls).to.include('Rollout Details:');
386+
expect(writeCalls).to.include(' Rollout Percentage: 50%');
387+
expect(writeCalls).to.include(' Devices in Rollout:');
388+
expect(writeCalls).to.include(' - device1');
389+
expect(writeCalls).to.include(' - device2');
390+
expect(writeCalls).to.include(' Environment Variables:');
391+
expect(writeCalls).to.include(' FOO: bar');
392+
expect(writeCalls).to.include(' BAZ: qux');
393+
});
394+
395+
it('should save rollout information to a file', async () => {
396+
const rolloutData = {
397+
percentage: 50,
398+
device_ids: ['device1', 'device2'],
399+
env: {
400+
FOO: 'bar',
401+
BAZ: 'qux'
402+
}
403+
};
404+
const tempDir = await mkdtemp(path.join(tmpdir(), 'rollout-test-'));
405+
const outputFile = path.join(tempDir, 'rollout.json');
406+
407+
sinon.stub(envVarsCommands.api, 'getRollout').resolves(rolloutData);
408+
409+
await envVarsCommands.rollout({ product: 'my-product', file: outputFile });
410+
411+
expect(envVarsCommands.ui.showBusySpinnerUntilResolved).calledWith('Getting rollout information...');
412+
const writtenContent = await require('node:fs/promises').readFile(outputFile, 'utf8');
413+
expect(JSON.parse(writtenContent)).to.deep.equal(rolloutData);
414+
expect(envVarsCommands.ui.write).to.have.been.calledWith(`Rollout information has been saved to ${outputFile}`);
372415
});
373416
});
374417
});

0 commit comments

Comments
 (0)