fix(dns): match apex records stored under any apex spelling

toSource always writes the apex as ".", but listRecords already accepted "" and
"@" as apex spellings on read. The upsert lookup compared sources strictly, so a
record stored under one of the other spellings would not have matched and the
upsert would have created a duplicate apex record instead of updating it.

Normalize the candidate's source before comparing, so read and match agree.

Reported by Greptile on #5257.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guillaume Juge 2026-09-01 16:28:04 +02:00
parent 2bc22c520d
commit be4a5da405
2 changed files with 31 additions and 1 deletions

View File

@ -213,6 +213,29 @@ describe("infomaniakClient.upsertRecord", () => {
expect(lastBody().source).toBe(".");
});
it.each([".", "", "@"])(
"matches an existing apex record stored with a %s source",
async (source) => {
mockFetch
.mockResolvedValueOnce(
ikSuccess([
{ id: 8, type: "A", source, target: "1.1.1.1", ttl: 3600 },
]),
)
.mockResolvedValueOnce(ikSuccess({ id: 8 }));
const result = await infomaniakClient.upsertRecord(config, {
zoneId: "example.com",
type: "A",
name: "example.com",
content: "1.2.3.4",
});
expect(result).toEqual({ id: "8" });
expect(lastCall()[1].method).toBe("PUT");
},
);
it("matches the existing apex record instead of creating a duplicate", async () => {
mockFetch
.mockResolvedValueOnce(

View File

@ -71,6 +71,12 @@ const toSource = (name: string, zone: string) => {
const toFqdn = (source: string, zone: string) =>
APEX_SOURCES.has(source) ? zone : `${source}.${zone}`;
// toSource always writes the apex as ".", so an existing record stored under one
// of the other apex spellings has to normalize to the same thing before it can
// be matched.
const normalizeSource = (source: string) =>
APEX_SOURCES.has(source) ? "." : source;
// TXT targets are stored quoted; keep Dokploy's view of them unquoted so that
// editing a record does not stack a new pair of quotes on every save.
const unquoteTarget = (target: string) => {
@ -146,7 +152,8 @@ export const infomaniakClient: DnsClient<InfomaniakConfig> = {
const existing = await listZoneRecords(config, record.zoneId);
const match = existing.find(
(candidate) =>
candidate.type === record.type && candidate.source === source,
candidate.type === record.type &&
normalizeSource(candidate.source) === source,
);
const body = JSON.stringify(recordPayload(record, record.zoneId));