Skip to content

Commit aefd8d6

Browse files
Claudehotlong
andauthored
Add unit tests for remote API commands and utilities
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 978289b commit aefd8d6

2 files changed

Lines changed: 384 additions & 0 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { describe, it, expect } from 'vitest';
2+
import AuthLogin from '../src/commands/auth/login';
3+
import AuthLogout from '../src/commands/auth/logout';
4+
import AuthWhoami from '../src/commands/auth/whoami';
5+
import DataQuery from '../src/commands/data/query';
6+
import DataGet from '../src/commands/data/get';
7+
import DataCreate from '../src/commands/data/create';
8+
import DataUpdate from '../src/commands/data/update';
9+
import DataDelete from '../src/commands/data/delete';
10+
import MetaList from '../src/commands/meta/list';
11+
import MetaGet from '../src/commands/meta/get';
12+
import MetaRegister from '../src/commands/meta/register';
13+
import MetaDelete from '../src/commands/meta/delete';
14+
15+
describe('Remote API Commands (oclif)', () => {
16+
describe('Auth Commands', () => {
17+
it('should have auth login command', () => {
18+
expect(AuthLogin.description).toContain('Authenticate');
19+
expect(AuthLogin.flags).toHaveProperty('url');
20+
expect(AuthLogin.flags).toHaveProperty('email');
21+
expect(AuthLogin.flags).toHaveProperty('password');
22+
expect(AuthLogin.flags).toHaveProperty('json');
23+
});
24+
25+
it('should have auth logout command', () => {
26+
expect(AuthLogout.description).toContain('Clear');
27+
expect(AuthLogout.flags).toHaveProperty('json');
28+
});
29+
30+
it('should have auth whoami command', () => {
31+
expect(AuthWhoami.description).toContain('session');
32+
expect(AuthWhoami.flags).toHaveProperty('url');
33+
expect(AuthWhoami.flags).toHaveProperty('token');
34+
expect(AuthWhoami.flags).toHaveProperty('format');
35+
});
36+
37+
it('auth commands should have examples', () => {
38+
expect(AuthLogin.examples).toBeDefined();
39+
expect(AuthLogin.examples.length).toBeGreaterThan(0);
40+
expect(AuthLogout.examples).toBeDefined();
41+
expect(AuthWhoami.examples).toBeDefined();
42+
});
43+
});
44+
45+
describe('Data Commands', () => {
46+
it('should have data query command', () => {
47+
expect(DataQuery.description).toContain('Query');
48+
expect(DataQuery.args).toHaveProperty('object');
49+
expect(DataQuery.flags).toHaveProperty('filter');
50+
expect(DataQuery.flags).toHaveProperty('fields');
51+
expect(DataQuery.flags).toHaveProperty('sort');
52+
expect(DataQuery.flags).toHaveProperty('limit');
53+
expect(DataQuery.flags).toHaveProperty('offset');
54+
expect(DataQuery.flags).toHaveProperty('format');
55+
});
56+
57+
it('should have data get command', () => {
58+
expect(DataGet.description).toContain('single record');
59+
expect(DataGet.args).toHaveProperty('object');
60+
expect(DataGet.args).toHaveProperty('id');
61+
expect(DataGet.flags).toHaveProperty('format');
62+
});
63+
64+
it('should have data create command', () => {
65+
expect(DataCreate.description).toContain('Create');
66+
expect(DataCreate.args).toHaveProperty('object');
67+
expect(DataCreate.flags).toHaveProperty('data');
68+
expect(DataCreate.flags).toHaveProperty('format');
69+
});
70+
71+
it('should have data update command', () => {
72+
expect(DataUpdate.description).toContain('Update');
73+
expect(DataUpdate.args).toHaveProperty('object');
74+
expect(DataUpdate.args).toHaveProperty('id');
75+
expect(DataUpdate.flags).toHaveProperty('data');
76+
expect(DataUpdate.flags).toHaveProperty('format');
77+
});
78+
79+
it('should have data delete command', () => {
80+
expect(DataDelete.description).toContain('Delete');
81+
expect(DataDelete.args).toHaveProperty('object');
82+
expect(DataDelete.args).toHaveProperty('id');
83+
expect(DataDelete.flags).toHaveProperty('format');
84+
});
85+
86+
it('data commands should support common flags', () => {
87+
const commands = [DataQuery, DataGet, DataCreate, DataUpdate, DataDelete];
88+
commands.forEach(cmd => {
89+
expect(cmd.flags).toHaveProperty('url');
90+
expect(cmd.flags).toHaveProperty('token');
91+
});
92+
});
93+
94+
it('data commands should have examples', () => {
95+
expect(DataQuery.examples).toBeDefined();
96+
expect(DataQuery.examples.length).toBeGreaterThan(0);
97+
expect(DataGet.examples).toBeDefined();
98+
expect(DataCreate.examples).toBeDefined();
99+
expect(DataUpdate.examples).toBeDefined();
100+
expect(DataDelete.examples).toBeDefined();
101+
});
102+
});
103+
104+
describe('Metadata Commands', () => {
105+
it('should have meta list command', () => {
106+
expect(MetaList.description).toContain('List metadata');
107+
expect(MetaList.args).toHaveProperty('type');
108+
expect(MetaList.flags).toHaveProperty('format');
109+
});
110+
111+
it('should have meta get command', () => {
112+
expect(MetaGet.description).toContain('Get');
113+
expect(MetaGet.args).toHaveProperty('type');
114+
expect(MetaGet.args).toHaveProperty('name');
115+
expect(MetaGet.flags).toHaveProperty('format');
116+
});
117+
118+
it('should have meta register command', () => {
119+
expect(MetaRegister.description).toContain('Register');
120+
expect(MetaRegister.args).toHaveProperty('type');
121+
expect(MetaRegister.flags).toHaveProperty('data');
122+
expect(MetaRegister.flags).toHaveProperty('format');
123+
});
124+
125+
it('should have meta delete command', () => {
126+
expect(MetaDelete.description).toContain('Delete');
127+
expect(MetaDelete.args).toHaveProperty('type');
128+
expect(MetaDelete.args).toHaveProperty('name');
129+
expect(MetaDelete.flags).toHaveProperty('format');
130+
});
131+
132+
it('meta commands should support common flags', () => {
133+
const commands = [MetaList, MetaGet, MetaRegister, MetaDelete];
134+
commands.forEach(cmd => {
135+
expect(cmd.flags).toHaveProperty('url');
136+
expect(cmd.flags).toHaveProperty('token');
137+
});
138+
});
139+
140+
it('meta commands should have examples', () => {
141+
expect(MetaList.examples).toBeDefined();
142+
expect(MetaList.examples.length).toBeGreaterThan(0);
143+
expect(MetaGet.examples).toBeDefined();
144+
expect(MetaRegister.examples).toBeDefined();
145+
expect(MetaDelete.examples).toBeDefined();
146+
});
147+
});
148+
149+
describe('Command Conventions', () => {
150+
it('all remote commands should support --url flag with OBJECTSTACK_URL env var', () => {
151+
const commands = [
152+
AuthLogin, AuthWhoami,
153+
DataQuery, DataGet, DataCreate, DataUpdate, DataDelete,
154+
MetaList, MetaGet, MetaRegister, MetaDelete
155+
];
156+
157+
commands.forEach(cmd => {
158+
expect(cmd.flags).toHaveProperty('url');
159+
expect(cmd.flags.url).toHaveProperty('env', 'OBJECTSTACK_URL');
160+
});
161+
});
162+
163+
it('authenticated commands should support --token flag with OBJECTSTACK_TOKEN env var', () => {
164+
const commands = [
165+
AuthWhoami,
166+
DataQuery, DataGet, DataCreate, DataUpdate, DataDelete,
167+
MetaList, MetaGet, MetaRegister, MetaDelete
168+
];
169+
170+
commands.forEach(cmd => {
171+
expect(cmd.flags).toHaveProperty('token');
172+
expect(cmd.flags.token).toHaveProperty('env', 'OBJECTSTACK_TOKEN');
173+
});
174+
});
175+
176+
it('all commands should support output formatting', () => {
177+
const commands = [
178+
AuthWhoami,
179+
DataQuery, DataGet, DataCreate, DataUpdate, DataDelete,
180+
MetaList, MetaGet, MetaRegister, MetaDelete
181+
];
182+
183+
commands.forEach(cmd => {
184+
expect(cmd.flags).toHaveProperty('format');
185+
});
186+
});
187+
});
188+
});
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { createApiClient, requireAuth } from '../src/utils/api-client';
3+
import { readAuthConfig, writeAuthConfig, deleteAuthConfig, getCredentialsPath } from '../src/utils/auth-config';
4+
import { formatOutput } from '../src/utils/output-formatter';
5+
import * as fs from 'node:fs/promises';
6+
7+
// Mock fs module
8+
vi.mock('node:fs/promises');
9+
10+
describe('API Client Utilities', () => {
11+
describe('createApiClient', () => {
12+
it('should use provided URL and token', async () => {
13+
const client = await createApiClient({
14+
url: 'https://test.example.com',
15+
token: 'test-token',
16+
});
17+
18+
expect(client).toBeDefined();
19+
expect((client as any).baseUrl).toBe('https://test.example.com');
20+
expect((client as any).token).toBe('test-token');
21+
});
22+
23+
it('should default to localhost when no URL provided', async () => {
24+
const client = await createApiClient({});
25+
26+
expect(client).toBeDefined();
27+
expect((client as any).baseUrl).toBe('http://localhost:3000');
28+
});
29+
30+
it('should use environment variables if no options provided', async () => {
31+
const originalUrl = process.env.OBJECTSTACK_URL;
32+
const originalToken = process.env.OBJECTSTACK_TOKEN;
33+
34+
process.env.OBJECTSTACK_URL = 'https://env.example.com';
35+
process.env.OBJECTSTACK_TOKEN = 'env-token';
36+
37+
const client = await createApiClient({});
38+
39+
expect((client as any).baseUrl).toBe('https://env.example.com');
40+
expect((client as any).token).toBe('env-token');
41+
42+
// Restore
43+
process.env.OBJECTSTACK_URL = originalUrl;
44+
process.env.OBJECTSTACK_TOKEN = originalToken;
45+
});
46+
});
47+
48+
describe('requireAuth', () => {
49+
it('should not throw when token is provided', () => {
50+
expect(() => requireAuth('valid-token')).not.toThrow();
51+
});
52+
53+
it('should throw when token is missing', () => {
54+
expect(() => requireAuth(undefined)).toThrow(/Authentication required/);
55+
});
56+
57+
it('should throw when token is empty string', () => {
58+
expect(() => requireAuth('')).toThrow(/Authentication required/);
59+
});
60+
});
61+
});
62+
63+
describe('Auth Config Utilities', () => {
64+
beforeEach(() => {
65+
vi.clearAllMocks();
66+
});
67+
68+
describe('getCredentialsPath', () => {
69+
it('should return path to credentials file', () => {
70+
const path = getCredentialsPath();
71+
expect(path).toContain('.objectstack');
72+
expect(path).toContain('credentials.json');
73+
});
74+
});
75+
76+
describe('writeAuthConfig', () => {
77+
it('should write credentials to file with correct permissions', async () => {
78+
const mockMkdir = vi.mocked(fs.mkdir);
79+
const mockWriteFile = vi.mocked(fs.writeFile);
80+
81+
const config = {
82+
url: 'https://test.example.com',
83+
token: 'test-token',
84+
email: 'user@example.com',
85+
createdAt: '2024-01-01T00:00:00.000Z',
86+
};
87+
88+
await writeAuthConfig(config);
89+
90+
expect(mockMkdir).toHaveBeenCalledWith(
91+
expect.stringContaining('.objectstack'),
92+
{ recursive: true }
93+
);
94+
95+
expect(mockWriteFile).toHaveBeenCalledWith(
96+
expect.stringContaining('credentials.json'),
97+
expect.stringContaining('test-token'),
98+
{ mode: 0o600 }
99+
);
100+
});
101+
});
102+
103+
describe('readAuthConfig', () => {
104+
it('should read and parse credentials file', async () => {
105+
const mockConfig = {
106+
url: 'https://test.example.com',
107+
token: 'test-token',
108+
email: 'user@example.com',
109+
createdAt: '2024-01-01T00:00:00.000Z',
110+
};
111+
112+
const mockReadFile = vi.mocked(fs.readFile);
113+
mockReadFile.mockResolvedValue(JSON.stringify(mockConfig));
114+
115+
const config = await readAuthConfig();
116+
117+
expect(config).toEqual(mockConfig);
118+
});
119+
120+
it('should throw helpful error when file does not exist', async () => {
121+
const mockReadFile = vi.mocked(fs.readFile);
122+
mockReadFile.mockRejectedValue({ code: 'ENOENT' });
123+
124+
await expect(readAuthConfig()).rejects.toThrow(/No stored credentials/);
125+
});
126+
});
127+
128+
describe('deleteAuthConfig', () => {
129+
it('should delete credentials file', async () => {
130+
const mockUnlink = vi.fn().mockResolvedValue(undefined);
131+
vi.doMock('node:fs/promises', () => ({
132+
unlink: mockUnlink,
133+
}));
134+
135+
await deleteAuthConfig();
136+
137+
// Should not throw
138+
});
139+
140+
it('should not throw if file does not exist', async () => {
141+
const mockUnlink = vi.fn().mockRejectedValue({ code: 'ENOENT' });
142+
vi.doMock('node:fs/promises', () => ({
143+
unlink: mockUnlink,
144+
}));
145+
146+
await expect(deleteAuthConfig()).resolves.not.toThrow();
147+
});
148+
});
149+
});
150+
151+
describe('Output Formatter Utilities', () => {
152+
beforeEach(() => {
153+
// Spy on console.log
154+
vi.spyOn(console, 'log').mockImplementation(() => {});
155+
});
156+
157+
it('should format JSON output', () => {
158+
const data = { name: 'test', value: 123 };
159+
formatOutput(data, 'json');
160+
161+
expect(console.log).toHaveBeenCalledWith(
162+
expect.stringContaining('"name": "test"')
163+
);
164+
});
165+
166+
it('should format YAML output', () => {
167+
const data = { name: 'test', value: 123 };
168+
formatOutput(data, 'yaml');
169+
170+
expect(console.log).toHaveBeenCalled();
171+
});
172+
173+
it('should format table output for arrays', () => {
174+
const data = [
175+
{ name: 'item1', value: 1 },
176+
{ name: 'item2', value: 2 },
177+
];
178+
formatOutput(data, 'table');
179+
180+
expect(console.log).toHaveBeenCalled();
181+
});
182+
183+
it('should format table output for single object', () => {
184+
const data = { name: 'test', value: 123 };
185+
formatOutput(data, 'table');
186+
187+
expect(console.log).toHaveBeenCalled();
188+
});
189+
190+
it('should handle empty arrays', () => {
191+
const data: any[] = [];
192+
formatOutput(data, 'table');
193+
194+
expect(console.log).toHaveBeenCalled();
195+
});
196+
});

0 commit comments

Comments
 (0)