| Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 13h ago | 1 | package api |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | func TestAuthHeaderAndPath(t *testing.T) { |
| 13 | var gotAuth, gotPath, gotAccept string |
| 14 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 15 | gotAuth = r.Header.Get("Authorization") |
| 16 | gotPath = r.URL.Path |
| 17 | gotAccept = r.Header.Get("Accept") |
| 18 | json.NewEncoder(w).Encode(User{Handle: "ricktester"}) |
| 19 | })) |
| 20 | defer srv.Close() |
| 21 | |
| 22 | c := New(srv.URL, "rickub_pat_secret") |
| 23 | u, err := c.GetUser(context.Background()) |
| 24 | if err != nil { |
| 25 | t.Fatalf("GetUser: %v", err) |
| 26 | } |
| 27 | if u.Handle != "ricktester" { |
| 28 | t.Errorf("handle = %q", u.Handle) |
| 29 | } |
| 30 | if gotAuth != "Bearer rickub_pat_secret" { |
| 31 | t.Errorf("auth header = %q", gotAuth) |
| 32 | } |
| 33 | if gotPath != "/api/v1/user" { |
| 34 | t.Errorf("path = %q", gotPath) |
| 35 | } |
| 36 | if gotAccept != "application/json" { |
| 37 | t.Errorf("accept = %q", gotAccept) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestErrorEnvelopeParsed(t *testing.T) { |
| 42 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 43 | w.WriteHeader(http.StatusNotFound) |
| 44 | w.Write([]byte(`{"error":{"code":"not_found","message":"repository not found"}}`)) |
| 45 | })) |
| 46 | defer srv.Close() |
| 47 | |
| 48 | c := New(srv.URL, "t") |
| 49 | _, err := c.GetRepo(context.Background(), "who", "what") |
| 50 | if err == nil { |
| 51 | t.Fatal("expected error") |
| 52 | } |
| 53 | var apiErr *APIError |
| 54 | if !errors.As(err, &apiErr) { |
| 55 | t.Fatalf("expected *APIError, got %T", err) |
| 56 | } |
| 57 | if apiErr.Status != http.StatusNotFound { |
| 58 | t.Errorf("status = %d", apiErr.Status) |
| 59 | } |
| 60 | if apiErr.Code != "not_found" { |
| 61 | t.Errorf("code = %q", apiErr.Code) |
| 62 | } |
| 63 | if apiErr.Message != "repository not found" { |
| 64 | t.Errorf("message = %q", apiErr.Message) |
| 65 | } |
| 66 | if apiErr.Error() != "repository not found (not_found)" { |
| 67 | t.Errorf("Error() = %q", apiErr.Error()) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestNonJSONErrorFallback(t *testing.T) { |
| 72 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 73 | w.WriteHeader(http.StatusBadGateway) |
| 74 | w.Write([]byte("upstream boom")) |
| 75 | })) |
| 76 | defer srv.Close() |
| 77 | |
| 78 | c := New(srv.URL, "t") |
| 79 | _, err := c.GetUser(context.Background()) |
| 80 | var apiErr *APIError |
| 81 | if !errors.As(err, &apiErr) { |
| 82 | t.Fatalf("expected *APIError, got %v", err) |
| 83 | } |
| 84 | if apiErr.Message != "upstream boom" { |
| 85 | t.Errorf("message = %q", apiErr.Message) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | func TestPaginationQueryAndDecode(t *testing.T) { |
| 90 | var gotQuery string |
| 91 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 92 | gotQuery = r.URL.RawQuery |
| 93 | json.NewEncoder(w).Encode(map[string]any{ |
| 94 | "page": 2, |
| 95 | "per_page": 5, |
| 96 | "has_next": true, |
| 97 | "items": []map[string]any{ |
| 98 | {"owner": "ricktester", "name": "demo", "full_name": "ricktester/demo", "visibility": "public"}, |
| 99 | }, |
| 100 | }) |
| 101 | })) |
| 102 | defer srv.Close() |
| 103 | |
| 104 | c := New(srv.URL, "t") |
| 105 | page, err := c.ListUserRepos(context.Background(), "ricktester", 2, 5) |
| 106 | if err != nil { |
| 107 | t.Fatalf("ListUserRepos: %v", err) |
| 108 | } |
| 109 | if gotQuery != "page=2&per_page=5" { |
| 110 | t.Errorf("query = %q", gotQuery) |
| 111 | } |
| 112 | if page.Page.Page != 2 || page.PerPage != 5 || !page.HasNext { |
| 113 | t.Errorf("page envelope = %+v", page.Page) |
| 114 | } |
| 115 | if len(page.Items) != 1 || page.Items[0].FullName != "ricktester/demo" { |
| 116 | t.Errorf("items = %+v", page.Items) |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | func TestSearchQueryEncoding(t *testing.T) { |
| 121 | var gotQuery string |
| 122 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 123 | gotQuery = r.URL.RawQuery |
| 124 | json.NewEncoder(w).Encode(RepoPage{}) |
| 125 | })) |
| 126 | defer srv.Close() |
| 127 | |
| 128 | c := New(srv.URL, "t") |
| 129 | if _, err := c.SearchRepos(context.Background(), "hello world", 0, 0); err != nil { |
| 130 | t.Fatalf("SearchRepos: %v", err) |
| 131 | } |
| 132 | if gotQuery != "q=hello+world" { |
| 133 | t.Errorf("query = %q", gotQuery) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | func TestCreateRepoSendsBody(t *testing.T) { |
| 138 | var gotMethod string |
| 139 | var body RepoCreate |
| 140 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 141 | gotMethod = r.Method |
| 142 | json.NewDecoder(r.Body).Decode(&body) |
| 143 | if ct := r.Header.Get("Content-Type"); ct != "application/json" { |
| 144 | t.Errorf("content-type = %q", ct) |
| 145 | } |
| 146 | w.WriteHeader(http.StatusCreated) |
| 147 | json.NewEncoder(w).Encode(Repo{FullName: "ricktester/demo", Visibility: "public"}) |
| 148 | })) |
| 149 | defer srv.Close() |
| 150 | |
| 151 | c := New(srv.URL, "t") |
| 152 | r, err := c.CreateRepo(context.Background(), RepoCreate{Name: "demo", Visibility: "public"}) |
| 153 | if err != nil { |
| 154 | t.Fatalf("CreateRepo: %v", err) |
| 155 | } |
| 156 | if gotMethod != http.MethodPost { |
| 157 | t.Errorf("method = %s", gotMethod) |
| 158 | } |
| 159 | if body.Name != "demo" || body.Visibility != "public" { |
| 160 | t.Errorf("body = %+v", body) |
| 161 | } |
| 162 | if r.FullName != "ricktester/demo" { |
| 163 | t.Errorf("full_name = %q", r.FullName) |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | func TestDeleteRepoNoContent(t *testing.T) { |
| 168 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 169 | if r.Method != http.MethodDelete { |
| 170 | t.Errorf("method = %s", r.Method) |
| 171 | } |
| 172 | w.WriteHeader(http.StatusNoContent) |
| 173 | })) |
| 174 | defer srv.Close() |
| 175 | |
| 176 | c := New(srv.URL, "t") |
| 177 | if err := c.DeleteRepo(context.Background(), "o", "r"); err != nil { |
| 178 | t.Fatalf("DeleteRepo: %v", err) |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | func TestRawTextAcceptHeader(t *testing.T) { |
| 183 | var gotAccept string |
| 184 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 185 | gotAccept = r.Header.Get("Accept") |
| 186 | w.Header().Set("Content-Type", "text/plain") |
| 187 | w.Write([]byte("hello\n")) |
| 188 | })) |
| 189 | defer srv.Close() |
| 190 | |
| 191 | c := New(srv.URL, "t") |
| 192 | data, err := c.GetRaw(context.Background(), "o", "r", "main", "README.md") |
| 193 | if err != nil { |
| 194 | t.Fatalf("GetRaw: %v", err) |
| 195 | } |
| 196 | if gotAccept != "text/plain" { |
| 197 | t.Errorf("accept = %q", gotAccept) |
| 198 | } |
| 199 | if string(data) != "hello\n" { |
| 200 | t.Errorf("data = %q", data) |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | func TestContentsPathEscaping(t *testing.T) { |
| 205 | var gotPath string |
| 206 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 207 | gotPath = r.URL.EscapedPath() |
| 208 | json.NewEncoder(w).Encode(Contents{Type: "file", File: &Blob{Path: "a/b.txt", Content: "hi"}}) |
| 209 | })) |
| 210 | defer srv.Close() |
| 211 | |
| 212 | c := New(srv.URL, "t") |
| 213 | if _, err := c.GetContents(context.Background(), "o", "r", "main", "dir/sub/file name.txt"); err != nil { |
| 214 | t.Fatalf("GetContents: %v", err) |
| 215 | } |
| 216 | // Each segment escaped, slashes preserved. |
| 217 | want := "/api/v1/repos/o/r/contents/main/dir/sub/file%20name.txt" |
| 218 | if gotPath != want { |
| 219 | t.Errorf("path = %q, want %q", gotPath, want) |
| 220 | } |
| 221 | } |