| @@ -0,0 +1,360 @@ |
| | | 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later |
| | | 2 | + | |
| | | 3 | + | package usecase_test |
| | | 4 | + | |
| | | 5 | + | import ( |
| | | 6 | + | "bytes" |
| | | 7 | + | "encoding/json" |
| | | 8 | + | "fmt" |
| | | 9 | + | "io" |
| | | 10 | + | "math" |
| | | 11 | + | "net/http" |
| | | 12 | + | "net/http/httptest" |
| | | 13 | + | "os" |
| | | 14 | + | "path/filepath" |
| | | 15 | + | "strconv" |
| | | 16 | + | "strings" |
| | | 17 | + | "testing" |
| | | 18 | + | |
| | | 19 | + | "bnema.dev/ermite/internal/adapters/githubapi" |
| | | 20 | + | "bnema.dev/ermite/internal/domain" |
| | | 21 | + | "bnema.dev/ermite/internal/ports" |
| | | 22 | + | "bnema.dev/ermite/internal/ports/mocks" |
| | | 23 | + | "bnema.dev/ermite/internal/usecase" |
| | | 24 | + | "github.com/stretchr/testify/mock" |
| | | 25 | + | "github.com/stretchr/testify/require" |
| | | 26 | + | ) |
| | | 27 | + | |
| | | 28 | + | func releaseGitHub(t *testing.T, r *releaseRig) *usecase.ReleasesGitHub { |
| | | 29 | + | t.Helper() |
| | | 30 | + | return usecase.NewReleasesGitHub(r.svc, "acme", "http://localhost:7341/") |
| | | 31 | + | } |
| | | 32 | + | func releaseGolden(t *testing.T, file string, body []byte) { |
| | | 33 | + | t.Helper() |
| | | 34 | + | var pretty bytes.Buffer |
| | | 35 | + | require.NoError(t, json.Indent(&pretty, body, "", " ")) |
| | | 36 | + | pretty.WriteByte('\n') |
| | | 37 | + | path := filepath.Join("..", "adapters", "githubapi", "testdata", "releases", file+".json") |
| | | 38 | + | if os.Getenv("UPDATE_RELEASE_GOLDENS") == "1" { |
| | | 39 | + | require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755)) |
| | | 40 | + | require.NoError(t, os.WriteFile(path, pretty.Bytes(), 0644)) |
| | | 41 | + | } |
| | | 42 | + | want, err := os.ReadFile(path) |
| | | 43 | + | require.NoError(t, err) |
| | | 44 | + | require.Equal(t, string(want), pretty.String()) |
| | | 45 | + | } |
| | | 46 | + | func TestGitHubReleaseGoldens(t *testing.T) { |
| | | 47 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 48 | + | g := releaseGitHub(t, r) |
| | | 49 | + | rel := r.create(t, "v1", false) |
| | | 50 | + | for _, name := range []string{"ermite.tar.gz", "checksums.txt"} { |
| | | 51 | + | _, err := g.UploadAsset(r.ctx, domain.Root(), "acme", r.repo.String(), rel.ID, name, "", "application/octet-stream", 3, strings.NewReader("abc")) |
| | | 52 | + | require.NoError(t, err) |
| | | 53 | + | } |
| | | 54 | + | body, err := g.GetRelease(r.ctx, domain.Anonymous(), "ACME", "RELEASES", ports.ReleaseRef{Tag: "v1"}) |
| | | 55 | + | require.NoError(t, err) |
| | | 56 | + | releaseGolden(t, "release", body) |
| | | 57 | + | draft := r.create(t, "next", true) |
| | | 58 | + | body, err = g.GetRelease(r.ctx, domain.Root(), "acme", r.repo.String(), ports.ReleaseRef{ID: draft.ID}) |
| | | 59 | + | require.NoError(t, err) |
| | | 60 | + | releaseGolden(t, "draft", body) |
| | | 61 | + | _, err = g.GetRelease(r.ctx, domain.Anonymous(), "acme", r.repo.String(), ports.ReleaseRef{ID: draft.ID}) |
| | | 62 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 63 | + | } |
| | | 64 | + | func TestGitHubReleaseRequests(t *testing.T) { |
| | | 65 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 66 | + | g := releaseGitHub(t, r) |
| | | 67 | + | repo := r.repo.String() |
| | | 68 | + | p := domain.Root() |
| | | 69 | + | r.resolve(t, "v1", nil) |
| | | 70 | + | body, err := g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"v1","name":"Title","body":"Notes","draft":true,"prerelease":true,"discussion_category_name":"ignored","make_latest":"legacy"}`)) |
| | | 71 | + | require.NoError(t, err) |
| | | 72 | + | var rel struct { |
| | | 73 | + | ID int64 `json:"id"` |
| | | 74 | + | } |
| | | 75 | + | require.NoError(t, json.Unmarshal(body, &rel)) |
| | | 76 | + | body, err = g.UpdateRelease(r.ctx, p, "acme", repo, rel.ID, []byte(`{"name":"","draft":false,"prerelease":false,"make_latest":"true","unknown":1}`)) |
| | | 77 | + | require.NoError(t, err) |
| | | 78 | + | var got map[string]any |
| | | 79 | + | require.NoError(t, json.Unmarshal(body, &got)) |
| | | 80 | + | require.Equal(t, "", got["name"]) |
| | | 81 | + | require.Equal(t, "Notes", got["body"]) |
| | | 82 | + | require.Equal(t, false, got["draft"]) |
| | | 83 | + | require.Equal(t, false, got["prerelease"]) |
| | | 84 | + | require.NotContains(t, got, "make_latest") |
| | | 85 | + | stored, err := r.svc.GetRelease(r.ctx, p, r.repo, ports.ReleaseRef{ID: rel.ID}) |
| | | 86 | + | require.NoError(t, err) |
| | | 87 | + | require.True(t, stored.MakeLatest) |
| | | 88 | + | _, err = g.UpdateRelease(r.ctx, p, "acme", repo, rel.ID, []byte(`{"make_latest":"legacy"}`)) |
| | | 89 | + | require.NoError(t, err) |
| | | 90 | + | stored, err = r.svc.GetRelease(r.ctx, p, r.repo, ports.ReleaseRef{ID: rel.ID}) |
| | | 91 | + | require.NoError(t, err) |
| | | 92 | + | require.True(t, stored.MakeLatest) |
| | | 93 | + | _, err = g.UpdateRelease(r.ctx, p, "acme", repo, rel.ID, []byte(`{"make_latest":"false"}`)) |
| | | 94 | + | require.NoError(t, err) |
| | | 95 | + | stored, err = r.svc.GetRelease(r.ctx, p, r.repo, ports.ReleaseRef{ID: rel.ID}) |
| | | 96 | + | require.NoError(t, err) |
| | | 97 | + | require.False(t, stored.MakeLatest) |
| | | 98 | + | for _, bad := range []string{`{}`, `null`, `[]`, `{`, `{"tag_name":2}`, `{"tag_name":"v1","draft":"true"}`, `{"tag_name":"v1","name":null}`, `{"tag_name":"v1","make_latest":true}`, `{"tag_name":"v1","make_latest":"yes"}`} { |
| | | 99 | + | _, err = g.CreateRelease(r.ctx, p, "acme", repo, []byte(bad)) |
| | | 100 | + | require.ErrorIs(t, err, domain.ErrInvalid, bad) |
| | | 101 | + | } |
| | | 102 | + | _, err = g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"v1"}`)) |
| | | 103 | + | require.ErrorIs(t, err, domain.ErrAlreadyExists) |
| | | 104 | + | _, err = g.GetRelease(r.ctx, p, "other", repo, ports.ReleaseRef{ID: rel.ID}) |
| | | 105 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 106 | + | _, err = g.GetRelease(r.ctx, p, "acme", "bad/name", ports.ReleaseRef{ID: rel.ID}) |
| | | 107 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 108 | + | } |
| | | 109 | + | func TestGitHubReleasePagination(t *testing.T) { |
| | | 110 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 111 | + | g := releaseGitHub(t, r) |
| | | 112 | + | for i := 0; i < 5; i++ { |
| | | 113 | + | r.create(t, "v"+strconv.Itoa(i), i == 4) |
| | | 114 | + | } |
| | | 115 | + | for page := 1; page <= 3; page++ { |
| | | 116 | + | b, link, err := g.ListReleases(r.ctx, domain.Root(), "acme", r.repo.String(), page, 2) |
| | | 117 | + | require.NoError(t, err) |
| | | 118 | + | var rows []struct { |
| | | 119 | + | ID int64 `json:"id"` |
| | | 120 | + | } |
| | | 121 | + | require.NoError(t, json.Unmarshal(b, &rows)) |
| | | 122 | + | require.Len(t, rows, min(2, 5-(page-1)*2)) |
| | | 123 | + | require.Equal(t, int64(5-(page-1)*2), rows[0].ID) |
| | | 124 | + | base := "http://localhost:7341/-/github/api/repos/acme/releases/releases?per_page=2&page=" |
| | | 125 | + | switch page { |
| | | 126 | + | case 1: |
| | | 127 | + | require.Equal(t, `<`+base+`2>; rel="next", <`+base+`3>; rel="last"`, link) |
| | | 128 | + | case 2: |
| | | 129 | + | require.Equal(t, `<`+base+`3>; rel="next", <`+base+`3>; rel="last", <`+base+`1>; rel="first", <`+base+`1>; rel="prev"`, link) |
| | | 130 | + | case 3: |
| | | 131 | + | require.Equal(t, `<`+base+`1>; rel="first", <`+base+`2>; rel="prev"`, link) |
| | | 132 | + | } |
| | | 133 | + | } |
| | | 134 | + | b, _, err := g.ListReleases(r.ctx, domain.Anonymous(), "acme", r.repo.String(), 0, 0) |
| | | 135 | + | require.NoError(t, err) |
| | | 136 | + | var rows []any |
| | | 137 | + | require.NoError(t, json.Unmarshal(b, &rows)) |
| | | 138 | + | require.Len(t, rows, 4) |
| | | 139 | + | for _, page := range []int{4, math.MaxInt} { |
| | | 140 | + | b, _, err = g.ListReleases(r.ctx, domain.Root(), "acme", r.repo.String(), page, 2) |
| | | 141 | + | require.NoError(t, err) |
| | | 142 | + | require.Equal(t, "[]", string(b)) |
| | | 143 | + | } |
| | | 144 | + | b, _, err = g.ListReleases(r.ctx, domain.Root(), "acme", r.repo.String(), 1, 1000) |
| | | 145 | + | require.NoError(t, err) |
| | | 146 | + | require.NoError(t, json.Unmarshal(b, &rows)) |
| | | 147 | + | require.Len(t, rows, 5) |
| | | 148 | + | } |
| | | 149 | + | func TestGitHubReleaseAssetsAndRetry(t *testing.T) { |
| | | 150 | + | r := newReleaseRig(t, ports.ReleaseLimits{AssetMax: 10}) |
| | | 151 | + | g := releaseGitHub(t, r) |
| | | 152 | + | p := domain.Root() |
| | | 153 | + | repo := r.repo.String() |
| | | 154 | + | rel := r.create(t, "v1/slash", false) |
| | | 155 | + | upload := func(name string) ([]byte, error) { |
| | | 156 | + | return g.UploadAsset(r.ctx, p, "acme", repo, rel.ID, name, "label", "text/plain", 3, strings.NewReader("abc")) |
| | | 157 | + | } |
| | | 158 | + | b, err := upload("é #?.txt") |
| | | 159 | + | require.NoError(t, err) |
| | | 160 | + | var asset struct { |
| | | 161 | + | ID int64 `json:"id"` |
| | | 162 | + | URL string `json:"browser_download_url"` |
| | | 163 | + | } |
| | | 164 | + | require.NoError(t, json.Unmarshal(b, &asset)) |
| | | 165 | + | require.Equal(t, "http://localhost:7341/releases/releases/download/v1%2Fslash/%C3%A9%20%23%3F.txt", asset.URL) |
| | | 166 | + | _, err = upload("é #?.txt") |
| | | 167 | + | require.ErrorIs(t, err, domain.ErrAlreadyExists) |
| | | 168 | + | b, err = g.UpdateAsset(r.ctx, p, "acme", repo, asset.ID, []byte(`{"label":""}`)) |
| | | 169 | + | require.NoError(t, err) |
| | | 170 | + | require.Contains(t, string(b), `"label":""`) |
| | | 171 | + | require.Contains(t, string(b), `"name":"é #?.txt"`) |
| | | 172 | + | b, err = g.GetAsset(r.ctx, domain.Anonymous(), "acme", repo, asset.ID) |
| | | 173 | + | require.NoError(t, err) |
| | | 174 | + | require.Contains(t, string(b), `"state":"uploaded"`) |
| | | 175 | + | for i := 0; i < 2; i++ { |
| | | 176 | + | _, err = upload(fmt.Sprint(i)) |
| | | 177 | + | require.NoError(t, err) |
| | | 178 | + | } |
| | | 179 | + | b, link, err := g.ListAssets(r.ctx, p, "acme", repo, rel.ID, 1, 2) |
| | | 180 | + | require.NoError(t, err) |
| | | 181 | + | require.Contains(t, link, `rel="next"`) |
| | | 182 | + | var assets []any |
| | | 183 | + | require.NoError(t, json.Unmarshal(b, &assets)) |
| | | 184 | + | require.Len(t, assets, 2) |
| | | 185 | + | require.NoError(t, g.DeleteAsset(r.ctx, p, "acme", repo, asset.ID)) |
| | | 186 | + | _, err = g.AssetByID(r.ctx, p, "acme", repo, asset.ID) |
| | | 187 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 188 | + | _, err = upload("é #?.txt") |
| | | 189 | + | require.NoError(t, err) |
| | | 190 | + | _, err = g.UploadAsset(r.ctx, p, "acme", repo, rel.ID, "huge", "", "", 11, strings.NewReader("ignored")) |
| | | 191 | + | require.ErrorIs(t, err, domain.ErrResourceExhausted) |
| | | 192 | + | require.NoError(t, g.DeleteRelease(r.ctx, p, "acme", repo, rel.ID)) |
| | | 193 | + | _, err = g.GetRelease(r.ctx, p, "acme", repo, ports.ReleaseRef{ID: rel.ID}) |
| | | 194 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 195 | + | } |
| | | 196 | + | |
| | | 197 | + | func TestGitHubReleaseAuthorizationAndLatest(t *testing.T) { |
| | | 198 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 199 | + | g := releaseGitHub(t, r) |
| | | 200 | + | repo := r.repo.String() |
| | | 201 | + | stable := r.create(t, "v1", false) |
| | | 202 | + | r.create(t, "draft", true) |
| | | 203 | + | b, err := g.GetRelease(r.ctx, domain.Anonymous(), "acme", repo, ports.ReleaseRef{Latest: true}) |
| | | 204 | + | require.NoError(t, err) |
| | | 205 | + | var got struct { |
| | | 206 | + | ID int64 `json:"id"` |
| | | 207 | + | } |
| | | 208 | + | require.NoError(t, json.Unmarshal(b, &got)) |
| | | 209 | + | require.Equal(t, stable.ID, got.ID) |
| | | 210 | + | writer := domain.RepoTokenPrincipal(domain.CIRepoToken{Repo: r.repo, Permissions: map[string]string{domain.PermContents: domain.PermissionWrite}}) |
| | | 211 | + | reader := domain.RepoTokenPrincipal(domain.CIRepoToken{Repo: r.repo, Permissions: map[string]string{domain.PermContents: domain.PermissionRead}}) |
| | | 212 | + | branched := domain.TokenPrincipal(domain.Token{Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeReleaseWrite}, BranchPrefixes: []string{"feature/"}}) |
| | | 213 | + | for _, tc := range []struct { |
| | | 214 | + | p domain.Principal |
| | | 215 | + | tag string |
| | | 216 | + | want error |
| | | 217 | + | }{ |
| | | 218 | + | {domain.Anonymous(), "anonymous", domain.ErrUnauthenticated}, |
| | | 219 | + | {reader, "reader", domain.ErrForbidden}, |
| | | 220 | + | {branched, "branch", domain.ErrForbidden}, |
| | | 221 | + | {writer, "job", nil}, |
| | | 222 | + | } { |
| | | 223 | + | if tc.want == nil { |
| | | 224 | + | r.resolve(t, tc.tag, nil) |
| | | 225 | + | } |
| | | 226 | + | b, err = g.CreateRelease(r.ctx, tc.p, "acme", repo, []byte(`{"tag_name":"`+tc.tag+`"}`)) |
| | | 227 | + | if tc.want != nil { |
| | | 228 | + | require.ErrorIs(t, err, tc.want) |
| | | 229 | + | continue |
| | | 230 | + | } |
| | | 231 | + | require.NoError(t, err) |
| | | 232 | + | require.NoError(t, json.Unmarshal(b, &got)) |
| | | 233 | + | _, err = g.UploadAsset(r.ctx, tc.p, "acme", repo, got.ID, "ci.tar.gz", "", "", 3, strings.NewReader("abc")) |
| | | 234 | + | require.NoError(t, err) |
| | | 235 | + | _, err = g.UploadAsset(r.ctx, reader, "acme", repo, got.ID, "read.tar.gz", "", "", 3, strings.NewReader("abc")) |
| | | 236 | + | require.ErrorIs(t, err, domain.ErrForbidden) |
| | | 237 | + | } |
| | | 238 | + | other, err := domain.ParseRepoName("other") |
| | | 239 | + | require.NoError(t, err) |
| | | 240 | + | require.NoError(t, r.store.Create(r.ctx, domain.Repo{Name: other, Visibility: domain.VisibilityPublic, CreatedAt: now})) |
| | | 241 | + | b, _, err = g.ListAssets(r.ctx, domain.Root(), "acme", other.String(), stable.ID, 1, 30) |
| | | 242 | + | require.ErrorIs(t, err, domain.ErrNotFound) |
| | | 243 | + | require.Nil(t, b) |
| | | 244 | + | } |
| | | 245 | + | |
| | | 246 | + | func TestGitHubReleaseTargetAndMissingTag(t *testing.T) { |
| | | 247 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 248 | + | g := releaseGitHub(t, r) |
| | | 249 | + | p := domain.Root() |
| | | 250 | + | repo := r.repo.String() |
| | | 251 | + | r.resolve(t, "missing", domain.ErrNotFound) |
| | | 252 | + | _, err := g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"missing"}`)) |
| | | 253 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) |
| | | 254 | + | b, err := g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"missing","draft":true}`)) |
| | | 255 | + | require.NoError(t, err) |
| | | 256 | + | var got struct { |
| | | 257 | + | ID int64 `json:"id"` |
| | | 258 | + | Target string `json:"target_commitish"` |
| | | 259 | + | } |
| | | 260 | + | require.NoError(t, json.Unmarshal(b, &got)) |
| | | 261 | + | require.Empty(t, got.Target) |
| | | 262 | + | _, err = g.UpdateRelease(r.ctx, p, "acme", repo, got.ID, []byte(`{"draft":false}`)) |
| | | 263 | + | require.ErrorIs(t, err, domain.ErrPreconditionFailed) |
| | | 264 | + | r.resolve(t, "new", domain.ErrNotFound) |
| | | 265 | + | v, err := domain.ParseRevision("main") |
| | | 266 | + | require.NoError(t, err) |
| | | 267 | + | oid, err := domain.ParseObjectID(oidA) |
| | | 268 | + | require.NoError(t, err) |
| | | 269 | + | r.reader.EXPECT().ResolveRevision(r.ctx, r.repo, v).Return(domain.ResolvedRevision{CommitID: oid}, nil).Once() |
| | | 270 | + | r.tags.EXPECT().CreateTag(r.ctx, r.repo, "new", oid).Return(nil).Once() |
| | | 271 | + | r.pushes.EXPECT().Record(r.ctx, p, r.repo, []domain.RefUpdate{{Ref: "refs/tags/new", Old: domain.ZeroOID, New: oidA}}).Return(nil).Once() |
| | | 272 | + | b, err = g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"new","target_commitish":"main"}`)) |
| | | 273 | + | require.NoError(t, err) |
| | | 274 | + | require.NoError(t, json.Unmarshal(b, &got)) |
| | | 275 | + | require.Equal(t, oidA, got.Target) |
| | | 276 | + | |
| | | 277 | + | // PATCH with target_commitish publishes a draft whose tag is missing. |
| | | 278 | + | r.resolve(t, "patched", domain.ErrNotFound) |
| | | 279 | + | b, err = g.CreateRelease(r.ctx, p, "acme", repo, []byte(`{"tag_name":"patched","draft":true}`)) |
| | | 280 | + | require.NoError(t, err) |
| | | 281 | + | require.NoError(t, json.Unmarshal(b, &got)) |
| | | 282 | + | r.reader.EXPECT().ResolveRevision(r.ctx, r.repo, v).Return(domain.ResolvedRevision{CommitID: oid}, nil).Once() |
| | | 283 | + | r.tags.EXPECT().CreateTag(r.ctx, r.repo, "patched", oid).Return(nil).Once() |
| | | 284 | + | r.pushes.EXPECT().Record(r.ctx, p, r.repo, []domain.RefUpdate{{Ref: "refs/tags/patched", Old: domain.ZeroOID, New: oidA}}).Return(nil).Once() |
| | | 285 | + | b, err = g.UpdateRelease(r.ctx, p, "acme", repo, got.ID, []byte(`{"draft":false,"target_commitish":"main"}`)) |
| | | 286 | + | require.NoError(t, err) |
| | | 287 | + | require.Contains(t, string(b), `"draft":false`) |
| | | 288 | + | require.Contains(t, string(b), `"target_commitish":"`+oidA+`"`) |
| | | 289 | + | } |
| | | 290 | + | |
| | | 291 | + | // A real HTTP round trip through the facade, metadata store and disk blob |
| | | 292 | + | // adapter protects the goreleaser/softprops create, retry, overwrite, publish |
| | | 293 | + | // sequence, not just each side's mocked contract. |
| | | 294 | + | func TestGitHubReleaseHTTPWorkflow(t *testing.T) { |
| | | 295 | + | r := newReleaseRig(t, ports.ReleaseLimits{}) |
| | | 296 | + | g := releaseGitHub(t, r) |
| | | 297 | + | auth := mocks.NewAuthService(t) |
| | | 298 | + | auth.EXPECT().Authenticate(mock.Anything, "dev-token").Return(domain.Root(), nil).Maybe() |
| | | 299 | + | srv := httptest.NewServer(githubapi.New(githubapi.Config{Auth: auth, Releases: g, ReleaseDownload: r.svc})) |
| | | 300 | + | defer srv.Close() |
| | | 301 | + | do := func(method, path, body string, headers map[string]string) (*http.Response, string) { |
| | | 302 | + | req, err := http.NewRequestWithContext(r.ctx, method, srv.URL+githubapi.Prefix+path, strings.NewReader(body)) |
| | | 303 | + | require.NoError(t, err) |
| | | 304 | + | req.Header.Set("Authorization", "Bearer dev-token") |
| | | 305 | + | for k, v := range headers { |
| | | 306 | + | req.Header.Set(k, v) |
| | | 307 | + | } |
| | | 308 | + | resp, err := srv.Client().Do(req) |
| | | 309 | + | require.NoError(t, err) |
| | | 310 | + | defer func() { _ = resp.Body.Close() }() |
| | | 311 | + | b, err := io.ReadAll(resp.Body) |
| | | 312 | + | require.NoError(t, err) |
| | | 313 | + | return resp, string(b) |
| | | 314 | + | } |
| | | 315 | + | base := "/repos/acme/" + r.repo.String() + "/releases" |
| | | 316 | + | resp, _ := do("GET", base+"/tags/v1", "", nil) |
| | | 317 | + | require.Equal(t, 404, resp.StatusCode) |
| | | 318 | + | r.resolve(t, "v1", nil) |
| | | 319 | + | resp, b := do("POST", base, `{"tag_name":"v1","draft":true}`, nil) |
| | | 320 | + | require.Equal(t, 201, resp.StatusCode) |
| | | 321 | + | var rel struct { |
| | | 322 | + | ID int64 `json:"id"` |
| | | 323 | + | UploadURL string `json:"upload_url"` |
| | | 324 | + | } |
| | | 325 | + | require.NoError(t, json.Unmarshal([]byte(b), &rel)) |
| | | 326 | + | upload := strings.TrimPrefix(strings.Split(rel.UploadURL, "{")[0], "http://localhost:7341"+githubapi.Prefix) + "?name=archive.tar.gz" |
| | | 327 | + | resp, b = do("POST", upload, "0123456789", map[string]string{"Content-Type": "application/gzip"}) |
| | | 328 | + | require.Equal(t, 201, resp.StatusCode) |
| | | 329 | + | var a struct { |
| | | 330 | + | ID int64 `json:"id"` |
| | | 331 | + | } |
| | | 332 | + | require.NoError(t, json.Unmarshal([]byte(b), &a)) |
| | | 333 | + | resp, b = do("POST", upload, "replacement", nil) |
| | | 334 | + | require.Equal(t, 422, resp.StatusCode) |
| | | 335 | + | require.Contains(t, b, `"code":"already_exists"`) |
| | | 336 | + | resp, b = do("GET", base+"/"+strconv.FormatInt(rel.ID, 10)+"/assets", "", nil) |
| | | 337 | + | require.Equal(t, 200, resp.StatusCode) |
| | | 338 | + | require.Contains(t, b, `"name":"archive.tar.gz"`) |
| | | 339 | + | assetURL := base + "/assets/" + strconv.FormatInt(a.ID, 10) |
| | | 340 | + | resp, _ = do("DELETE", assetURL, "", nil) |
| | | 341 | + | require.Equal(t, 204, resp.StatusCode) |
| | | 342 | + | resp, b = do("POST", upload, "abcdefghij", nil) |
| | | 343 | + | require.Equal(t, 201, resp.StatusCode) |
| | | 344 | + | require.NoError(t, json.Unmarshal([]byte(b), &a)) |
| | | 345 | + | resp, _ = do("PATCH", base+"/"+strconv.FormatInt(rel.ID, 10), `{"draft":false}`, nil) |
| | | 346 | + | require.Equal(t, 200, resp.StatusCode) |
| | | 347 | + | assetURL = base + "/assets/" + strconv.FormatInt(a.ID, 10) |
| | | 348 | + | resp, b = do("GET", assetURL, "", map[string]string{"Accept": "application/octet-stream", "Range": "bytes=2-5"}) |
| | | 349 | + | require.Equal(t, 206, resp.StatusCode) |
| | | 350 | + | require.Equal(t, "cdef", b) |
| | | 351 | + | require.Equal(t, "bytes 2-5/10", resp.Header.Get("Content-Range")) |
| | | 352 | + | require.Equal(t, "4", resp.Header.Get("Content-Length")) |
| | | 353 | + | require.Empty(t, resp.Header.Get("Location")) |
| | | 354 | + | resp, b = do("HEAD", assetURL, "", map[string]string{"Accept": "application/octet-stream"}) |
| | | 355 | + | require.Equal(t, 200, resp.StatusCode) |
| | | 356 | + | require.Empty(t, b) |
| | | 357 | + | require.Equal(t, "10", resp.Header.Get("Content-Length")) |
| | | 358 | + | resp, _ = do("GET", assetURL, "", map[string]string{"Accept": "application/octet-stream", "If-None-Match": resp.Header.Get("ETag")}) |
| | | 359 | + | require.Equal(t, 304, resp.StatusCode) |
| | | 360 | + | } |