mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 03:24:59 +02:00
fix(core): Preserve local credential fields absent from source contro… (#34173)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fcdd756122
commit
0a4dddeb29
|
|
@ -1188,7 +1188,7 @@ describe('Source Control Helper', () => {
|
|||
expect(result.port).toBe(5000); // Number from remote
|
||||
});
|
||||
|
||||
it('should only include fields present in remote', () => {
|
||||
it('should keep local fields that are absent from remote', () => {
|
||||
const local = {
|
||||
apiKey: 'secret',
|
||||
port: 3000,
|
||||
|
|
@ -1197,14 +1197,32 @@ describe('Source Control Helper', () => {
|
|||
const remote = {
|
||||
port: 8080,
|
||||
apiKey: '', // Plain string sanitized to empty
|
||||
// extraField is NOT in remote, so won't be in result
|
||||
// extraField is NOT in remote (e.g. left at its default when pushed)
|
||||
};
|
||||
|
||||
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
|
||||
|
||||
expect(result.apiKey).toBe('secret'); // Local preserved (empty skipped)
|
||||
expect(result.port).toBe(8080); // Merged from remote
|
||||
expect(result.extraField).toBeUndefined(); // Not in remote, not in result
|
||||
expect(result.extraField).toBe('local-only'); // Absent from remote, local retained
|
||||
});
|
||||
|
||||
it('should not reset a locally selected option field the remote stub omits', () => {
|
||||
// A stub pushed from an instance where the option was left at its default
|
||||
// omits that field entirely. Pulling it must not reset a different local selection.
|
||||
const local = {
|
||||
apikey: 'prod-secret',
|
||||
environment: 'https://api.example.com', // non-default selection on this instance
|
||||
};
|
||||
const remote = {
|
||||
apikey: '', // secret blanked in the stub
|
||||
// environment omitted because the pushing instance used the default
|
||||
} as ICredentialDataDecryptedObject;
|
||||
|
||||
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
|
||||
|
||||
expect(result.apikey).toBe('prod-secret');
|
||||
expect(result.environment).toBe('https://api.example.com');
|
||||
});
|
||||
|
||||
it('should handle empty local object', () => {
|
||||
|
|
@ -1229,8 +1247,8 @@ describe('Source Control Helper', () => {
|
|||
|
||||
const result = mergeRemoteCrendetialDataIntoLocalCredentialData({ local, remote });
|
||||
|
||||
// Remote is empty, so result is empty (remote is source of truth for which fields exist)
|
||||
expect(result).toEqual({});
|
||||
// Remote carries no fields, so every local field is retained rather than wiped
|
||||
expect(result).toEqual({ apiKey: 'secret', port: 3000 });
|
||||
});
|
||||
|
||||
it('should handle both empty objects', () => {
|
||||
|
|
|
|||
|
|
@ -141,11 +141,15 @@ export function mergeRemoteCrendetialDataIntoLocalCredentialData({
|
|||
}
|
||||
}
|
||||
|
||||
// Because oauthTokenData is explicitly stripped from the remote data during sanitization,
|
||||
// it will never exist in the sanitizedRemote object. Therefore, it is skipped in the loop above.
|
||||
// We manually merge it back from local to prevent OAuth credentials being wiped out on pull.
|
||||
if (local.oauthTokenData) {
|
||||
merged.oauthTokenData = local.oauthTokenData;
|
||||
// Keep local fields the remote stub does not carry. A field left at its default value
|
||||
// is not persisted, so it never reaches the stub; an absent field carries the same
|
||||
// "no value to give" meaning as a present-but-blank one, which is already preserved
|
||||
// above. Without this it would be dropped on pull and reset to its default. This also
|
||||
// covers oauthTokenData, which sanitization always strips from the remote.
|
||||
for (const [key, localValue] of Object.entries(local)) {
|
||||
if (!(key in sanitizedRemote)) {
|
||||
merged[key] = localValue;
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user