Skip to content

Commit 14ac40b

Browse files
authored
fix: remove stale connection files from directory export when connections are deleted (#1389)
* fix: remove stale connection files from directory export when connections are deleted * test: remove redundant tests * style: run prettier on connections test file * fix: remove stale connection files from directory export when connections are deleted * fix: preserve excluded connection files and remove stale files on directory dump
1 parent 4c067b7 commit 14ac40b

3 files changed

Lines changed: 101 additions & 0 deletions

File tree

src/context/directory/handlers/connections.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ async function dump(context: DirectoryContext): Promise<void> {
7878
const connectionsFolder = path.join(context.filePath, constants.CONNECTIONS_DIRECTORY);
7979
fs.ensureDirSync(connectionsFolder);
8080

81+
// Track files that should remain after dump (written + excluded).
82+
// Seed from exclude names directly so excluded connections' files are preserved
83+
// regardless of whether the connection still exists on the tenant.
84+
const expectedFiles = new Set<string>();
85+
for (const name of excludedConnections) {
86+
expectedFiles.add(`${sanitize(name)}.json`);
87+
expectedFiles.add(`${sanitize(name)}.html`);
88+
}
89+
8190
// Convert enabled_clients from id to name
8291
connections.forEach((connection) => {
8392
let dumpedConnection = {
@@ -118,7 +127,19 @@ async function dump(context: DirectoryContext): Promise<void> {
118127

119128
const connectionFile = path.join(connectionsFolder, `${connectionName}.json`);
120129
dumpJSON(connectionFile, dumpedConnection);
130+
expectedFiles.add(`${connectionName}.json`);
131+
if (dumpedConnection.strategy === 'email') expectedFiles.add(`${connectionName}.html`);
121132
});
133+
134+
// Remove files that belong to connections no longer present (and not excluded)
135+
if (fs.existsSync(connectionsFolder)) {
136+
for (const existing of fs.readdirSync(connectionsFolder)) {
137+
const fullPath = path.join(connectionsFolder, existing);
138+
if (fs.statSync(fullPath).isFile() && !expectedFiles.has(existing)) {
139+
fs.removeSync(fullPath);
140+
}
141+
}
142+
}
122143
}
123144

124145
const connectionsHandler: DirectoryHandler<ParsedConnections> = {

src/context/directory/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ export default class DirectoryContext {
112112
this.assets = auth0.assets;
113113
}
114114

115+
// Re-attach exclude/include config lost when assets was overwritten above
116+
this.assets.exclude = {
117+
rules: this.config.AUTH0_EXCLUDED_RULES || [],
118+
clients: this.config.AUTH0_EXCLUDED_CLIENTS || [],
119+
databases: this.config.AUTH0_EXCLUDED_DATABASES || [],
120+
connections: this.config.AUTH0_EXCLUDED_CONNECTIONS || [],
121+
resourceServers: this.config.AUTH0_EXCLUDED_RESOURCE_SERVERS || [],
122+
defaults: this.config.AUTH0_EXCLUDED_DEFAULTS || [],
123+
};
124+
this.assets.include = {
125+
connections: this.config.AUTH0_INCLUDED_CONNECTIONS || [],
126+
};
127+
115128
// Clean known read only fields
116129
this.assets = cleanAssets(this.assets, this.config);
117130

test/context/directory/connections.test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,71 @@ describe('#directory context connections', () => {
243243
expect(fs.existsSync(path.join(connectionsFolder, 'includedConnection.json'))).to.equal(true);
244244
expect(fs.existsSync(path.join(connectionsFolder, 'excludedConnection.json'))).to.equal(false);
245245
});
246+
247+
it('should preserve pre-existing files for excluded connections on re-dump', async () => {
248+
const dir = path.join(testDataDir, 'directory', 'connectionsDumpExclude');
249+
cleanThenMkdir(dir);
250+
251+
// Simulate a prior export that wrote the excluded connection's file
252+
const connectionsFolder = path.join(dir, constants.CONNECTIONS_DIRECTORY);
253+
fs.ensureDirSync(connectionsFolder);
254+
fs.writeFileSync(
255+
path.join(connectionsFolder, 'excludedConnection.json'),
256+
'{"name":"excludedConnection"}'
257+
);
258+
259+
const context = new Context({ AUTH0_INPUT_FILE: dir }, mockMgmtClient());
260+
context.assets.connections = [{ name: 'includedConnection', strategy: 'waad' }];
261+
context.assets.exclude = { connections: ['excludedConnection'] };
262+
263+
await handler.dump(context);
264+
265+
expect(fs.existsSync(path.join(connectionsFolder, 'includedConnection.json'))).to.equal(true);
266+
expect(fs.existsSync(path.join(connectionsFolder, 'excludedConnection.json'))).to.equal(true);
267+
});
268+
269+
it('should remove stale connection files when connections are removed from source', async () => {
270+
const dir = path.join(testDataDir, 'directory', 'connectionsStaleFiles');
271+
cleanThenMkdir(dir);
272+
273+
// Simulate a previous export that created connection files
274+
const connectionsFolder = path.join(dir, constants.CONNECTIONS_DIRECTORY);
275+
fs.ensureDirSync(connectionsFolder);
276+
fs.writeFileSync(path.join(connectionsFolder, 'facebook.json'), '{"name":"facebook"}');
277+
fs.writeFileSync(path.join(connectionsFolder, 'github.json'), '{"name":"github"}');
278+
279+
const context = new Context({ AUTH0_INPUT_FILE: dir }, mockMgmtClient());
280+
// Re-export with only one connection (facebook removed)
281+
context.assets.connections = [{ name: 'github', strategy: 'github' }];
282+
283+
await handler.dump(context);
284+
285+
expect(fs.existsSync(path.join(connectionsFolder, 'github.json'))).to.equal(true);
286+
expect(fs.existsSync(path.join(connectionsFolder, 'facebook.json'))).to.equal(false);
287+
});
288+
289+
it('should remove all stale connection files when all connections are removed from source', async () => {
290+
const dir = path.join(testDataDir, 'directory', 'connectionsAllStale');
291+
cleanThenMkdir(dir);
292+
293+
// Simulate a previous export that created connection files
294+
const connectionsFolder = path.join(dir, constants.CONNECTIONS_DIRECTORY);
295+
fs.ensureDirSync(connectionsFolder);
296+
fs.writeFileSync(path.join(connectionsFolder, 'facebook.json'), '{"name":"facebook"}');
297+
fs.writeFileSync(path.join(connectionsFolder, 'github.json'), '{"name":"github"}');
298+
299+
const context = new Context({ AUTH0_INPUT_FILE: dir }, mockMgmtClient());
300+
// Re-export with zero connections
301+
context.assets.connections = [];
302+
303+
await handler.dump(context);
304+
305+
const remainingJsonFiles = fs.readdirSync(connectionsFolder).filter((f) => f.endsWith('.json'));
306+
expect(remainingJsonFiles).to.deep.equal([]);
307+
308+
// Verify parse of now-empty directory returns [] not null (enabling deletions on import)
309+
const parseContext = new Context({ AUTH0_INPUT_FILE: dir }, mockMgmtClient());
310+
await parseContext.loadAssetsFromLocal();
311+
expect(parseContext.assets.connections).to.deep.equal([]);
312+
});
246313
});

0 commit comments

Comments
 (0)