Skip to content

Commit d4877a7

Browse files
committed
feat(appstash): add createConfigStore() for context and credential management
1 parent 023d77a commit d4877a7

File tree

3 files changed

+528
-0
lines changed

3 files changed

+528
-0
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
import { createConfigStore } from '../src';
5+
6+
describe('createConfigStore', () => {
7+
let tempBase: string;
8+
9+
beforeEach(() => {
10+
tempBase = fs.mkdtempSync(path.join(os.tmpdir(), 'appstash-config-test-'));
11+
});
12+
13+
afterEach(() => {
14+
if (fs.existsSync(tempBase)) {
15+
fs.rmSync(tempBase, { recursive: true, force: true });
16+
}
17+
});
18+
19+
function createStore(toolName = 'testapp') {
20+
return createConfigStore(toolName, { baseDir: tempBase });
21+
}
22+
23+
describe('settings', () => {
24+
it('should return default settings when none exist', () => {
25+
const store = createStore();
26+
const settings = store.loadSettings();
27+
expect(settings).toEqual({});
28+
});
29+
30+
it('should save and load settings', () => {
31+
const store = createStore();
32+
store.saveSettings({ currentContext: 'production' });
33+
const settings = store.loadSettings();
34+
expect(settings.currentContext).toBe('production');
35+
});
36+
37+
it('should overwrite existing settings', () => {
38+
const store = createStore();
39+
store.saveSettings({ currentContext: 'staging' });
40+
store.saveSettings({ currentContext: 'production' });
41+
const settings = store.loadSettings();
42+
expect(settings.currentContext).toBe('production');
43+
});
44+
});
45+
46+
describe('context management', () => {
47+
it('should create a context', () => {
48+
const store = createStore();
49+
const ctx = store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
50+
expect(ctx.name).toBe('production');
51+
expect(ctx.endpoint).toBe('https://api.example.com/graphql');
52+
expect(ctx.createdAt).toBeDefined();
53+
expect(ctx.updatedAt).toBeDefined();
54+
});
55+
56+
it('should load a created context', () => {
57+
const store = createStore();
58+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
59+
const ctx = store.loadContext('production');
60+
expect(ctx).not.toBeNull();
61+
expect(ctx!.name).toBe('production');
62+
expect(ctx!.endpoint).toBe('https://api.example.com/graphql');
63+
});
64+
65+
it('should return null for non-existent context', () => {
66+
const store = createStore();
67+
const ctx = store.loadContext('nonexistent');
68+
expect(ctx).toBeNull();
69+
});
70+
71+
it('should list all contexts', () => {
72+
const store = createStore();
73+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
74+
store.createContext('staging', { endpoint: 'https://staging.example.com/graphql' });
75+
const contexts = store.listContexts();
76+
expect(contexts).toHaveLength(2);
77+
const names = contexts.map(c => c.name).sort();
78+
expect(names).toEqual(['production', 'staging']);
79+
});
80+
81+
it('should return empty list when no contexts exist', () => {
82+
const store = createStore();
83+
const contexts = store.listContexts();
84+
expect(contexts).toEqual([]);
85+
});
86+
87+
it('should delete a context', () => {
88+
const store = createStore();
89+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
90+
const deleted = store.deleteContext('production');
91+
expect(deleted).toBe(true);
92+
expect(store.loadContext('production')).toBeNull();
93+
});
94+
95+
it('should return false when deleting non-existent context', () => {
96+
const store = createStore();
97+
const deleted = store.deleteContext('nonexistent');
98+
expect(deleted).toBe(false);
99+
});
100+
101+
it('should clear currentContext when deleting the active context', () => {
102+
const store = createStore();
103+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
104+
store.setCurrentContext('production');
105+
store.deleteContext('production');
106+
const settings = store.loadSettings();
107+
expect(settings.currentContext).toBeUndefined();
108+
});
109+
});
110+
111+
describe('current context', () => {
112+
it('should return null when no current context is set', () => {
113+
const store = createStore();
114+
const ctx = store.getCurrentContext();
115+
expect(ctx).toBeNull();
116+
});
117+
118+
it('should set and get current context', () => {
119+
const store = createStore();
120+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
121+
const result = store.setCurrentContext('production');
122+
expect(result).toBe(true);
123+
const ctx = store.getCurrentContext();
124+
expect(ctx).not.toBeNull();
125+
expect(ctx!.name).toBe('production');
126+
});
127+
128+
it('should return false when setting non-existent context as current', () => {
129+
const store = createStore();
130+
const result = store.setCurrentContext('nonexistent');
131+
expect(result).toBe(false);
132+
});
133+
});
134+
135+
describe('credentials', () => {
136+
it('should return null for non-existent credentials', () => {
137+
const store = createStore();
138+
const creds = store.getCredentials('production');
139+
expect(creds).toBeNull();
140+
});
141+
142+
it('should set and get credentials', () => {
143+
const store = createStore();
144+
store.setCredentials('production', { token: 'abc123' });
145+
const creds = store.getCredentials('production');
146+
expect(creds).not.toBeNull();
147+
expect(creds!.token).toBe('abc123');
148+
});
149+
150+
it('should store optional credential fields', () => {
151+
const store = createStore();
152+
store.setCredentials('production', {
153+
token: 'abc123',
154+
expiresAt: '2099-12-31T23:59:59Z',
155+
refreshToken: 'refresh456',
156+
});
157+
const creds = store.getCredentials('production');
158+
expect(creds!.expiresAt).toBe('2099-12-31T23:59:59Z');
159+
expect(creds!.refreshToken).toBe('refresh456');
160+
});
161+
162+
it('should overwrite existing credentials', () => {
163+
const store = createStore();
164+
store.setCredentials('production', { token: 'old' });
165+
store.setCredentials('production', { token: 'new' });
166+
const creds = store.getCredentials('production');
167+
expect(creds!.token).toBe('new');
168+
});
169+
170+
it('should remove credentials', () => {
171+
const store = createStore();
172+
store.setCredentials('production', { token: 'abc123' });
173+
const removed = store.removeCredentials('production');
174+
expect(removed).toBe(true);
175+
expect(store.getCredentials('production')).toBeNull();
176+
});
177+
178+
it('should return false when removing non-existent credentials', () => {
179+
const store = createStore();
180+
const removed = store.removeCredentials('production');
181+
expect(removed).toBe(false);
182+
});
183+
184+
it('should keep credentials for other contexts when removing one', () => {
185+
const store = createStore();
186+
store.setCredentials('production', { token: 'prod-token' });
187+
store.setCredentials('staging', { token: 'staging-token' });
188+
store.removeCredentials('production');
189+
expect(store.getCredentials('production')).toBeNull();
190+
expect(store.getCredentials('staging')!.token).toBe('staging-token');
191+
});
192+
});
193+
194+
describe('hasValidCredentials', () => {
195+
it('should return false when no credentials exist', () => {
196+
const store = createStore();
197+
expect(store.hasValidCredentials('production')).toBe(false);
198+
});
199+
200+
it('should return true for valid non-expiring token', () => {
201+
const store = createStore();
202+
store.setCredentials('production', { token: 'abc123' });
203+
expect(store.hasValidCredentials('production')).toBe(true);
204+
});
205+
206+
it('should return true for token with future expiry', () => {
207+
const store = createStore();
208+
store.setCredentials('production', {
209+
token: 'abc123',
210+
expiresAt: '2099-12-31T23:59:59Z',
211+
});
212+
expect(store.hasValidCredentials('production')).toBe(true);
213+
});
214+
215+
it('should return false for expired token', () => {
216+
const store = createStore();
217+
store.setCredentials('production', {
218+
token: 'abc123',
219+
expiresAt: '2020-01-01T00:00:00Z',
220+
});
221+
expect(store.hasValidCredentials('production')).toBe(false);
222+
});
223+
224+
it('should return false for empty token', () => {
225+
const store = createStore();
226+
store.setCredentials('production', { token: '' });
227+
expect(store.hasValidCredentials('production')).toBe(false);
228+
});
229+
});
230+
231+
describe('isolation between tools', () => {
232+
it('should isolate config between different tool names', () => {
233+
const store1 = createStore('app1');
234+
const store2 = createStore('app2');
235+
236+
store1.createContext('production', { endpoint: 'https://app1.example.com/graphql' });
237+
store2.createContext('production', { endpoint: 'https://app2.example.com/graphql' });
238+
239+
const ctx1 = store1.loadContext('production');
240+
const ctx2 = store2.loadContext('production');
241+
242+
expect(ctx1!.endpoint).toBe('https://app1.example.com/graphql');
243+
expect(ctx2!.endpoint).toBe('https://app2.example.com/graphql');
244+
});
245+
246+
it('should isolate credentials between different tool names', () => {
247+
const store1 = createStore('app1');
248+
const store2 = createStore('app2');
249+
250+
store1.setCredentials('production', { token: 'token1' });
251+
store2.setCredentials('production', { token: 'token2' });
252+
253+
expect(store1.getCredentials('production')!.token).toBe('token1');
254+
expect(store2.getCredentials('production')!.token).toBe('token2');
255+
});
256+
});
257+
258+
describe('full workflow', () => {
259+
it('should support the complete context + auth workflow', () => {
260+
const store = createStore();
261+
262+
// Create contexts
263+
store.createContext('production', { endpoint: 'https://api.example.com/graphql' });
264+
store.createContext('staging', { endpoint: 'https://staging.example.com/graphql' });
265+
266+
// Set current context
267+
store.setCurrentContext('production');
268+
expect(store.getCurrentContext()!.name).toBe('production');
269+
270+
// Set credentials
271+
store.setCredentials('production', { token: 'prod-token' });
272+
expect(store.hasValidCredentials('production')).toBe(true);
273+
274+
// Switch context
275+
store.setCurrentContext('staging');
276+
expect(store.getCurrentContext()!.name).toBe('staging');
277+
expect(store.hasValidCredentials('staging')).toBe(false);
278+
279+
// Auth staging
280+
store.setCredentials('staging', { token: 'staging-token' });
281+
expect(store.hasValidCredentials('staging')).toBe(true);
282+
283+
// List contexts
284+
const contexts = store.listContexts();
285+
expect(contexts).toHaveLength(2);
286+
287+
// Logout from production
288+
store.removeCredentials('production');
289+
expect(store.hasValidCredentials('production')).toBe(false);
290+
expect(store.hasValidCredentials('staging')).toBe(true);
291+
292+
// Delete staging
293+
store.deleteContext('staging');
294+
expect(store.listContexts()).toHaveLength(1);
295+
expect(store.getCurrentContext()).toBeNull();
296+
});
297+
});
298+
});

0 commit comments

Comments
 (0)