fix(store,api): идемпотентный import (UNIQUE+ON CONFLICT) + PATCH привязки шаблона к домену
This commit is contained in:
@@ -143,3 +143,137 @@ func TestImportDomains_RollsBackAllOnError(t *testing.T) {
|
||||
t.Fatalf("expected 0 domains after rollback, got %d", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// TestImportDomains_IdempotentOnRepeat verifies the fix for the import
|
||||
// idempotency gap: re-importing the same zones must not create duplicate
|
||||
// domains (enforced by the domains_project_zone_uniq constraint + ON
|
||||
// CONFLICT DO NOTHING in the ImportDomain query) and must not error.
|
||||
func TestImportDomains_IdempotentOnRepeat(t *testing.T) {
|
||||
s, ctx := newStore(t)
|
||||
acc, err := s.Queries().CreateAccount(ctx, db.CreateAccountParams{
|
||||
ID: uuid.New(), ProjectID: defaultProject, Provider: "selectel", SecretEnc: "enc-blob",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
zones := []provider.Zone{
|
||||
{ID: "z1", Name: "a.example.com"},
|
||||
{ID: "z2", Name: "b.example.com"},
|
||||
}
|
||||
first, err := s.ImportDomains(ctx, defaultProject, acc.ID, zones)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(first) != 2 {
|
||||
t.Fatalf("expected 2 domains on first import, got %d", len(first))
|
||||
}
|
||||
|
||||
second, err := s.ImportDomains(ctx, defaultProject, acc.ID, zones)
|
||||
if err != nil {
|
||||
t.Fatalf("expected repeat import to succeed idempotently, got error: %v", err)
|
||||
}
|
||||
if len(second) != 0 {
|
||||
t.Fatalf("expected 0 newly-created domains on repeat import, got %d", len(second))
|
||||
}
|
||||
|
||||
list, err := s.ListDomains(ctx, defaultProject)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("expected still exactly 2 domains (no duplicates), got %d", len(list))
|
||||
}
|
||||
|
||||
var count int
|
||||
row := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM domains WHERE project_id = $1 AND zone_id = $2`, defaultProject, "z1")
|
||||
if err := row.Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("expected COUNT=1 for zone z1 (UNIQUE constraint), got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetDomainTemplate_ClosesImportCheckLoop verifies the fix for the
|
||||
// second review gap: an imported domain (template_id=NULL) can be bound to
|
||||
// a template via SetDomainTemplate, after which LoadDomain succeeds and
|
||||
// returns that template — closing the import -> bind -> check cycle.
|
||||
func TestSetDomainTemplate_ClosesImportCheckLoop(t *testing.T) {
|
||||
s, ctx := newStore(t)
|
||||
acc, err := s.Queries().CreateAccount(ctx, db.CreateAccountParams{
|
||||
ID: uuid.New(), ProjectID: defaultProject, Provider: "selectel", SecretEnc: "enc-blob",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doms, err := s.ImportDomains(ctx, defaultProject, acc.ID, []provider.Zone{{ID: "z1", Name: "a.example.com"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dom := doms[0]
|
||||
|
||||
// Before binding, the domain is not checkable.
|
||||
if _, err := s.LoadDomain(ctx, dom.ID); err == nil {
|
||||
t.Fatal("expected LoadDomain to fail before a template is bound")
|
||||
}
|
||||
|
||||
doc := dto.TemplateDoc{Records: []dto.RecordDTO{
|
||||
{Type: "A", Name: "www.a.example.com.", TTL: 300, Values: []string{"1.2.3.4"}},
|
||||
}}
|
||||
tpl, err := s.CreateTemplate(ctx, defaultProject, "base", doc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
updated, err := s.SetDomainTemplate(ctx, dom.ID, defaultProject, &tpl.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated.TemplateID == nil || *updated.TemplateID != tpl.ID {
|
||||
t.Fatalf("expected domain.TemplateID=%s, got %+v", tpl.ID, updated.TemplateID)
|
||||
}
|
||||
|
||||
ref, err := s.LoadDomain(ctx, dom.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("expected LoadDomain to succeed after binding template, got error: %v", err)
|
||||
}
|
||||
if len(ref.Template.Records) != 1 || ref.Template.Records[0].Type != "A" {
|
||||
t.Fatalf("unexpected template loaded: %+v", ref.Template)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetDomainTemplate_RejectsForeignProjectTemplate verifies that binding
|
||||
// a template belonging to a different project is rejected rather than
|
||||
// silently succeeding (which would let one tenant's domain use another
|
||||
// tenant's DNS template).
|
||||
func TestSetDomainTemplate_RejectsForeignProjectTemplate(t *testing.T) {
|
||||
s, ctx := newStore(t)
|
||||
acc, err := s.Queries().CreateAccount(ctx, db.CreateAccountParams{
|
||||
ID: uuid.New(), ProjectID: defaultProject, Provider: "selectel", SecretEnc: "enc-blob",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doms, err := s.ImportDomains(ctx, defaultProject, acc.ID, []provider.Zone{{ID: "z1", Name: "a.example.com"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dom := doms[0]
|
||||
|
||||
// A template that belongs to a different (foreign) project. The default
|
||||
// user is the seed tenant from migrations/0001_init.sql.
|
||||
defaultUser := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
foreignProject := uuid.New()
|
||||
if _, err := s.pool.Exec(ctx, `INSERT INTO projects (id, user_id, name) VALUES ($1, $2, 'foreign')`, foreignProject, defaultUser); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foreignTpl, err := s.CreateTemplate(ctx, foreignProject, "foreign", dto.TemplateDoc{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := s.SetDomainTemplate(ctx, dom.ID, defaultProject, &foreignTpl.ID); err == nil {
|
||||
t.Fatal("expected error binding a template from a different project, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user