package api import ( "net/http" "testing" ) // The zone routes answer 501 rather than 404 when nothing is wired up behind // them: a missing route reads as "this build has no zone support", which is a // different thing from "this embedder owns no zones". The fake's hooks are // installed on the live server, so clearing them is enough. func TestZoneRoutesWithoutHooks(t *testing.T) { cases := []struct { method string path string body string }{ {http.MethodGet, "/api/zones", ""}, {http.MethodPost, "/api/zones/records", `{"zone":"example.com.","name":"a","type":"A","value":"10.0.0.1"}`}, {http.MethodPut, "/api/zones/records", `{"zone":"example.com.","name":"a","type":"A","value":"10.0.0.1"}`}, {http.MethodDelete, "/api/zones/records?zone=example.com.&name=a&type=A", ""}, {http.MethodPost, "/api/zones/delegations", `{"zone":"example.com.","name":"sub","targets":["ns1.sub.example.com."]}`}, {http.MethodDelete, "/api/zones/delegations?zone=example.com.&name=sub.example.com.", ""}, } for _, tc := range cases { t.Run(tc.method+" "+tc.path, func(t *testing.T) { h := newHarness(t, nil) h.srv.hooks.ListZones = nil h.srv.hooks.AddZoneRecord = nil h.srv.hooks.PutZoneRecord = nil h.srv.hooks.DeleteZoneRecord = nil h.srv.hooks.AddZoneDelegation = nil h.srv.hooks.DeleteZoneDelegation = nil res := h.do(tc.method, tc.path, tc.body, "X-API-Key: rw-key") if res.status != http.StatusNotImplemented { t.Errorf("%s %s = %d, want 501: %s", tc.method, tc.path, res.status, res.body) } }) } } // An incomplete record is refused with a message about the field, before the // write path is reached: the console shows the error verbatim. func TestZoneRecordValidation(t *testing.T) { cases := []struct { name string body string want string }{ {"no zone", `{"name":"a.example.com.","type":"A","value":"10.0.0.1"}`, "zone is required"}, {"no name", `{"zone":"example.com.","type":"A","value":"10.0.0.1"}`, "name is required"}, {"no type", `{"zone":"example.com.","name":"a","value":"10.0.0.1"}`, "type is required"}, {"no value", `{"zone":"example.com.","name":"a","type":"A"}`, "value is required"}, {"malformed", `{`, "malformed request body"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { h := newHarness(t, nil) res := h.do(http.MethodPost, "/api/zones/records", tc.body, "X-API-Key: rw-key") if res.status != http.StatusBadRequest { t.Fatalf("POST /api/zones/records = %d, want 400: %s", res.status, res.body) } if got := res.json(t)["error"]; got != tc.want { t.Errorf("error = %v, want %q", got, tc.want) } if h.fake.saw("AddZoneRecord") { t.Error("an invalid record reached the write path") } }) } } func TestZoneDelegationValidation(t *testing.T) { cases := []struct { name string path string body string want string }{ {"no zone", "/api/zones/delegations", `{"name":"sub","targets":["ns1.sub.example.com."]}`, "zone is required"}, {"no name", "/api/zones/delegations", `{"zone":"example.com.","targets":["ns1.sub.example.com."]}`, "name is required"}, {"no targets", "/api/zones/delegations", `{"zone":"example.com.","name":"sub"}`, "a delegation needs at least one NS target"}, {"delete without params", "/api/zones/delegations", "", "zone and name are required"}, {"record delete without params", "/api/zones/records", "", "zone, name and type are required"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { h := newHarness(t, nil) method := http.MethodPost if tc.body == "" { method = http.MethodDelete } res := h.do(method, tc.path, tc.body, "X-API-Key: rw-key") if res.status != http.StatusBadRequest { t.Fatalf("%s %s = %d, want 400: %s", method, tc.path, res.status, res.body) } if got := res.json(t)["error"]; got != tc.want { t.Errorf("error = %v, want %q", got, tc.want) } }) } } // A hook error is a 404 on the delete routes and a 400 on the writes, the same // split the domain routes use: a delete that matched nothing is a stale // console, while a rejected record is bad input. func TestZoneHookErrorsAreReported(t *testing.T) { t.Run("add", func(t *testing.T) { h := newHarness(t, nil) setFail(h, "AddZoneRecord", errHook) res := h.do(http.MethodPost, "/api/zones/records", `{"zone":"example.com.","name":"a","type":"A","value":"10.0.0.1"}`, "X-API-Key: rw-key") if res.status != http.StatusBadRequest { t.Errorf("POST = %d, want 400: %s", res.status, res.body) } }) t.Run("delete", func(t *testing.T) { h := newHarness(t, nil) setFail(h, "DeleteZoneRecord", errHook) res := h.do(http.MethodDelete, "/api/zones/records?zone=example.com.&name=a&type=A", "", "X-API-Key: rw-key") if res.status != http.StatusNotFound { t.Errorf("DELETE = %d, want 404: %s", res.status, res.body) } }) t.Run("list", func(t *testing.T) { h := newHarness(t, nil) setFail(h, "ListZones", errHook) res := h.do(http.MethodGet, "/api/zones", "", "X-API-Key: rw-key") if res.status != http.StatusInternalServerError { t.Errorf("GET = %d, want 500: %s", res.status, res.body) } }) } // A read-only key may look at the zones and may not change them. func TestZoneWritesNeedAWriteKey(t *testing.T) { h := newHarness(t, nil) if res := h.do(http.MethodGet, "/api/zones", "", "X-API-Key: ro-key"); res.status != http.StatusOK { t.Errorf("read with a read-only key = %d, want 200: %s", res.status, res.body) } writes := []struct { method string path string body string }{ {http.MethodPost, "/api/zones/records", `{"zone":"example.com.","name":"a","type":"A","value":"10.0.0.1"}`}, {http.MethodPut, "/api/zones/records", `{"zone":"example.com.","name":"a","type":"A","value":"10.0.0.1"}`}, {http.MethodDelete, "/api/zones/records?zone=example.com.&name=a&type=A", ""}, {http.MethodPost, "/api/zones/delegations", `{"zone":"example.com.","name":"sub","targets":["ns1.sub.example.com."]}`}, {http.MethodDelete, "/api/zones/delegations?zone=example.com.&name=sub.example.com.", ""}, } for _, w := range writes { res := h.do(w.method, w.path, w.body, "X-API-Key: ro-key") if res.status != http.StatusForbidden { t.Errorf("%s %s with a read-only key = %d, want 403: %s", w.method, w.path, res.status, res.body) } } }