mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
feat: snap can be queried by revision and ``track/risk/branch`` (#4439)
--------- Signed-off-by: Yuntao Hu <victorhu493@gmail.com>
This commit is contained in:
parent
74c9380248
commit
c9760d2341
@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@ -31,21 +32,28 @@ type remoteSnap struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
const NotSpecifiedRevision int = 0
|
||||
|
||||
type snapIdentity struct {
|
||||
Name string
|
||||
Channel string
|
||||
Architecture string
|
||||
Revision int
|
||||
}
|
||||
|
||||
func (s snapIdentity) String() string {
|
||||
parts := []string{s.Name}
|
||||
// revision will supersede channel
|
||||
if s.Revision != NotSpecifiedRevision {
|
||||
parts = append(parts, fmt.Sprintf(":%d", s.Revision))
|
||||
} else {
|
||||
if s.Channel != "" {
|
||||
parts = append(parts, fmt.Sprintf("@%s", s.Channel))
|
||||
}
|
||||
|
||||
if s.Channel != "" {
|
||||
parts = append(parts, fmt.Sprintf("@%s", s.Channel))
|
||||
}
|
||||
|
||||
if s.Architecture != "" {
|
||||
parts = append(parts, fmt.Sprintf(" (%s)", s.Architecture))
|
||||
if s.Architecture != "" {
|
||||
parts = append(parts, fmt.Sprintf(" (%s)", s.Architecture))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
@ -166,17 +174,21 @@ func getSnapFileInfo(ctx context.Context, fs afero.Fs, path string, hashes []cry
|
||||
// The request can be:
|
||||
// - A snap name (e.g., "etcd")
|
||||
// - A snap name with channel (e.g., "etcd@beta" or "etcd@2.3/stable")
|
||||
// - A snap name with revision (e.g. etcd:249@stable)
|
||||
func resolveRemoteSnap(request, architecture string) (*remoteSnap, error) {
|
||||
if architecture == "" {
|
||||
architecture = defaultArchitecture
|
||||
}
|
||||
|
||||
snapName, channel := parseSnapRequest(request)
|
||||
|
||||
snapName, revision, channel, err := parseSnapRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id := snapIdentity{
|
||||
Name: snapName,
|
||||
Channel: channel,
|
||||
Architecture: architecture,
|
||||
Revision: revision,
|
||||
}
|
||||
|
||||
client := newSnapcraftClient()
|
||||
@ -194,15 +206,26 @@ func resolveRemoteSnap(request, architecture string) (*remoteSnap, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSnapRequest parses a snap request into name and channel
|
||||
// parseSnapRequest parses a snap request into name and revision/channel
|
||||
// Examples:
|
||||
// - "etcd" -> name="etcd", channel="stable" (default)
|
||||
// - "etcd@beta" -> name="etcd", channel="beta"
|
||||
// - "etcd@2.3/stable" -> name="etcd", channel="2.3/stable"
|
||||
func parseSnapRequest(request string) (name, channel string) {
|
||||
// - "etcd:249@2.3/stable" -> name="etcd" revision=249 (channel not working because revision has been assigned)
|
||||
func parseSnapRequest(request string) (name string, revision int, channel string, err error) {
|
||||
parts := strings.SplitN(request, "@", 2)
|
||||
name = parts[0]
|
||||
|
||||
divisions := strings.Split(parts[0], ":")
|
||||
// handle revision first
|
||||
if len(divisions) == 2 {
|
||||
name = divisions[0]
|
||||
revision, err = strconv.Atoi(divisions[1])
|
||||
if err != nil {
|
||||
return "", NotSpecifiedRevision, "", err
|
||||
}
|
||||
return name, revision, "", err
|
||||
}
|
||||
if len(parts) == 2 {
|
||||
channel = parts[1]
|
||||
}
|
||||
@ -210,8 +233,7 @@ func parseSnapRequest(request string) (name, channel string) {
|
||||
if channel == "" {
|
||||
channel = defaultChannel
|
||||
}
|
||||
|
||||
return name, channel
|
||||
return name, NotSpecifiedRevision, channel, err
|
||||
}
|
||||
|
||||
func downloadSnap(getter intFile.Getter, info *remoteSnap, dest string) error {
|
||||
|
||||
@ -508,78 +508,109 @@ func TestDownloadSnap(t *testing.T) {
|
||||
|
||||
func TestParseSnapRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request string
|
||||
expectedName string
|
||||
expectedChannel string
|
||||
name string
|
||||
request string
|
||||
expectedName string
|
||||
expectedRevision int
|
||||
expectedChannel string
|
||||
expectedError require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "snap name only - uses default channel",
|
||||
request: "etcd",
|
||||
expectedName: "etcd",
|
||||
expectedChannel: "stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with beta channel",
|
||||
request: "etcd@beta",
|
||||
expectedName: "etcd",
|
||||
expectedChannel: "beta",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with edge channel",
|
||||
request: "etcd@edge",
|
||||
expectedName: "etcd",
|
||||
expectedChannel: "edge",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with version track",
|
||||
request: "etcd@2.3/stable",
|
||||
expectedName: "etcd",
|
||||
expectedChannel: "2.3/stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with complex channel path",
|
||||
request: "mysql@8.0/candidate",
|
||||
expectedName: "mysql",
|
||||
expectedChannel: "8.0/candidate",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with multiple @ symbols - only first is delimiter",
|
||||
request: "app@beta@test",
|
||||
expectedName: "app",
|
||||
expectedChannel: "beta@test",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with revision",
|
||||
request: "etcd:249",
|
||||
expectedName: "etcd",
|
||||
expectedRevision: 249,
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap with revision so the channel doesn't work",
|
||||
request: "etcd:249@2.3/beta",
|
||||
expectedName: "etcd",
|
||||
expectedRevision: 249,
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "empty snap name with channel",
|
||||
request: "@stable",
|
||||
expectedName: "",
|
||||
expectedChannel: "stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap name with empty channel - uses default",
|
||||
request: "etcd@",
|
||||
expectedName: "etcd",
|
||||
expectedChannel: "stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "hyphenated snap name",
|
||||
request: "hello-world@stable",
|
||||
expectedName: "hello-world",
|
||||
expectedChannel: "stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "snap name with numbers",
|
||||
request: "app123",
|
||||
expectedName: "app123",
|
||||
expectedChannel: "stable",
|
||||
expectedError: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
name, channel := parseSnapRequest(tt.request)
|
||||
name, revision, channel, err := parseSnapRequest(tt.request)
|
||||
assert.Equal(t, tt.expectedName, name)
|
||||
assert.Equal(t, tt.expectedChannel, channel)
|
||||
if tt.expectedRevision != NotSpecifiedRevision {
|
||||
assert.Equal(t, tt.expectedRevision, revision)
|
||||
} else {
|
||||
assert.Equal(t, tt.expectedChannel, channel)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
@ -58,17 +61,133 @@ type snapFindResponse struct {
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type SnapRisk string
|
||||
|
||||
const (
|
||||
RiskStable SnapRisk = "stable"
|
||||
RiskCandidate SnapRisk = "candidate"
|
||||
RiskBeta SnapRisk = "beta"
|
||||
RiskEdge SnapRisk = "edge"
|
||||
RiskUnknown SnapRisk = "unknown"
|
||||
)
|
||||
|
||||
func isValidSnapRisk(r SnapRisk) bool {
|
||||
switch r {
|
||||
case RiskStable, RiskCandidate, RiskBeta, RiskEdge:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func stringToSnapRisk(s string) SnapRisk {
|
||||
r := SnapRisk(s)
|
||||
if !isValidSnapRisk(r) {
|
||||
return RiskUnknown
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func getRevisionFromURL(cm snapChannelMapEntry) (rev int, err error) {
|
||||
re := regexp.MustCompile(`(\d+)\.snap$`)
|
||||
match := re.FindStringSubmatch(cm.Download.URL)
|
||||
if len(match) < 2 {
|
||||
err = fmt.Errorf("could not determine revision from %s", cm.Download.URL)
|
||||
return
|
||||
}
|
||||
rev, err = strconv.Atoi(match[1])
|
||||
return
|
||||
}
|
||||
|
||||
// isEligibleChannel determines whether a candidate channel satisfies a requested
|
||||
// channel. Both channels are parsed into {track, risk} pairs.
|
||||
//
|
||||
// Matching rules:
|
||||
// - If the request includes a track, both track and risk must match exactly.
|
||||
// - If the request omits the track (e.g., "stable"), any candidate track is
|
||||
// accepted as long as the risk matches.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// candidate="3.2/stable", request="stable" -> true
|
||||
// candidate="3.2/stable", request="3.2/stable" -> true
|
||||
// candidate="3.2/stable", request="3.2/beta" -> false
|
||||
// candidate="3.2/beta", request="stable" -> false
|
||||
// candidate="3.2/alpha", request="alpha" -> false(alpha is an invalid risk level)
|
||||
// candidate="3.2/stable/fix-for-bug123", request="stable" -> true
|
||||
// candidate="3.2/stable/fix-for-bug123", request="3.2/stable" -> true
|
||||
func isEligibleChannel(candidate, request string) (bool, error) {
|
||||
cTrack, cRisk, cBranch := splitChannel(candidate)
|
||||
rTrack, rRisk, rBranch := splitChannel(request)
|
||||
if rTrack == "" && rRisk == "" && rBranch == "" {
|
||||
return false, fmt.Errorf("there is no such risk in the channel(only stable/candidate/beta/edge are valid)")
|
||||
}
|
||||
|
||||
if rTrack != "" {
|
||||
return cTrack == rTrack && cRisk == rRisk && (cBranch == rBranch || rBranch == ""), nil
|
||||
}
|
||||
|
||||
return cRisk == rRisk && (cBranch == rBranch || rBranch == ""), nil
|
||||
}
|
||||
|
||||
func splitChannel(ch string) (track string, risk string, branch string) {
|
||||
parts := strings.SplitN(ch, "/", 3)
|
||||
if stringToSnapRisk(parts[0]) != RiskUnknown {
|
||||
if len(parts) == 1 {
|
||||
return "", parts[0], "" // no track
|
||||
} else if len(parts) == 2 {
|
||||
return "", parts[0], parts[1]
|
||||
}
|
||||
} else if len(parts) >= 2 && stringToSnapRisk(parts[1]) != RiskUnknown {
|
||||
if len(parts) == 3 {
|
||||
return parts[0], parts[1], parts[2]
|
||||
} else if len(parts) == 2 {
|
||||
return parts[0], parts[1], ""
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
func matchSnapDownloadURL(cm snapChannelMapEntry, id snapIdentity) (string, error) {
|
||||
// revision will supersede channel
|
||||
if id.Revision != NotSpecifiedRevision {
|
||||
rev, err2 := getRevisionFromURL(cm)
|
||||
if err2 == nil && rev == id.Revision {
|
||||
return cm.Download.URL, nil
|
||||
}
|
||||
} else if cm.Channel.Architecture == id.Architecture {
|
||||
matched, err2 := isEligibleChannel(cm.Channel.Name, id.Channel)
|
||||
if err2 != nil {
|
||||
return "", err2
|
||||
}
|
||||
if matched {
|
||||
return cm.Download.URL, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetSnapDownloadURL retrieves the download URL for a snap package
|
||||
func (c *snapcraftClient) GetSnapDownloadURL(id snapIdentity) (string, error) {
|
||||
apiURL := c.InfoAPIURL + id.Name
|
||||
|
||||
log.WithFields("name", id.Name, "channel", id.Channel, "architecture", id.Architecture).Trace("requesting snap info")
|
||||
if id.Revision == NotSpecifiedRevision {
|
||||
log.WithFields("name", id.Name, "channel", id.Channel, "architecture", id.Architecture).Trace("requesting snap info")
|
||||
} else {
|
||||
log.WithFields("name", id.Name, "revision", id.Revision, "architecture", id.Architecture).Trace("requesting snap info")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
if id.Revision != NotSpecifiedRevision {
|
||||
q := req.URL.Query()
|
||||
q.Add("revision", fmt.Sprintf("%d", id.Revision))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
req.Header.Set("Snap-Device-Series", defaultSeries)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
@ -107,9 +226,11 @@ func (c *snapcraftClient) GetSnapDownloadURL(id snapIdentity) (string, error) {
|
||||
}
|
||||
|
||||
for _, cm := range info.ChannelMap {
|
||||
if cm.Channel.Architecture == id.Architecture && cm.Channel.Name == id.Channel {
|
||||
return cm.Download.URL, nil
|
||||
url, err2 := matchSnapDownloadURL(cm, id)
|
||||
if url == "" && err2 == nil {
|
||||
continue
|
||||
}
|
||||
return url, err2
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no matching snap found for %s", id.String())
|
||||
|
||||
@ -161,6 +161,126 @@ func TestSnapcraftClient_GetSnapDownloadURL(t *testing.T) {
|
||||
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
expectError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "successful download URL retrieval (w/ track)",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "3.2/stable",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
expectError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "successful download URL retrieval (w/ track&branch)",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "3.2/stable/fix-for-bug123",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
expectError: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "branch unmatched",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable/fix-for-bug124",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "3.2/stable/fix-for-bug123",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: require.Error,
|
||||
errorContains: "no matching snap found",
|
||||
},
|
||||
{
|
||||
name: "risk unmatched",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "latest/beta",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: require.Error,
|
||||
errorContains: "no matching snap found",
|
||||
},
|
||||
{
|
||||
name: "illegal risk",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "foobar",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "latest/beta",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/etcd_123.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: require.Error,
|
||||
errorContains: "there is no such risk",
|
||||
},
|
||||
{
|
||||
name: "region-locked snap - exists but unavailable",
|
||||
snapID: snapIdentity{
|
||||
@ -351,6 +471,214 @@ func TestSnapcraftClient_GetSnapDownloadURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapcraftClient_GetSnapDownloadURL_WithVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
snapID snapIdentity
|
||||
infoResponse snapcraftInfo
|
||||
infoStatusCode int
|
||||
findResponse *snapFindResponse
|
||||
findStatusCode int
|
||||
expectedURL string
|
||||
expectError require.ErrorAssertionFunc
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "successful download URL retrieval",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
Revision: 249,
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "stable",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/TKebVGcPeDKoOqAmNmczU2oWLtsojKD5_249.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/TKebVGcPeDKoOqAmNmczU2oWLtsojKD5_249.snap",
|
||||
expectError: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.expectError == nil {
|
||||
tt.expectError = require.NoError
|
||||
}
|
||||
|
||||
infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
|
||||
|
||||
expectedPath := "/" + tt.snapID.Name
|
||||
assert.Equal(t, expectedPath, r.URL.Path)
|
||||
|
||||
w.WriteHeader(tt.infoStatusCode)
|
||||
|
||||
if tt.infoStatusCode == http.StatusOK {
|
||||
responseBytes, err := json.Marshal(tt.infoResponse)
|
||||
require.NoError(t, err)
|
||||
w.Write(responseBytes)
|
||||
}
|
||||
}))
|
||||
defer infoServer.Close()
|
||||
|
||||
var findServer *httptest.Server
|
||||
if tt.findResponse != nil || tt.findStatusCode != 0 {
|
||||
findServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
|
||||
assert.Equal(t, tt.snapID.Name, r.URL.Query().Get("name-startswith"))
|
||||
|
||||
statusCode := tt.findStatusCode
|
||||
if statusCode == 0 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if tt.findResponse != nil && statusCode == http.StatusOK {
|
||||
responseBytes, err := json.Marshal(tt.findResponse)
|
||||
require.NoError(t, err)
|
||||
w.Write(responseBytes)
|
||||
}
|
||||
}))
|
||||
defer findServer.Close()
|
||||
}
|
||||
|
||||
client := &snapcraftClient{
|
||||
InfoAPIURL: infoServer.URL + "/",
|
||||
HTTPClient: &http.Client{},
|
||||
}
|
||||
if findServer != nil {
|
||||
client.FindAPIURL = findServer.URL
|
||||
}
|
||||
|
||||
url, err := client.GetSnapDownloadURL(tt.snapID)
|
||||
tt.expectError(t, err)
|
||||
if err != nil {
|
||||
if tt.errorContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expectedURL, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapcraftClient_GetSnapDownloadURL_DoesntExist(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
snapID snapIdentity
|
||||
infoResponse snapcraftInfo
|
||||
infoStatusCode int
|
||||
findResponse *snapFindResponse
|
||||
findStatusCode int
|
||||
expectedURL string
|
||||
expectError require.ErrorAssertionFunc
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
name: "non-existent snap with revision",
|
||||
snapID: snapIdentity{
|
||||
Name: "etcd",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
Revision: 248,
|
||||
},
|
||||
infoStatusCode: http.StatusOK,
|
||||
infoResponse: snapcraftInfo{
|
||||
ChannelMap: []snapChannelMapEntry{
|
||||
{
|
||||
Channel: snapChannel{
|
||||
Architecture: "amd64",
|
||||
Name: "stable",
|
||||
},
|
||||
Download: snapDownload{
|
||||
URL: "https://api.snapcraft.io/api/v1/snaps/download/TKebVGcPeDKoOqAmNmczU2oWLtsojKD5_249.snap",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedURL: "https://api.snapcraft.io/api/v1/snaps/download/TKebVGcPeDKoOqAmNmczU2oWLtsojKD5_249.snap",
|
||||
expectError: func(t require.TestingT, err error, msgAndArgs ...interface{}) {
|
||||
require.EqualError(t, err, "no matching snap found for etcd:248")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.expectError == nil {
|
||||
tt.expectError = require.NoError
|
||||
}
|
||||
|
||||
infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
|
||||
|
||||
expectedPath := "/" + tt.snapID.Name
|
||||
assert.Equal(t, expectedPath, r.URL.Path)
|
||||
|
||||
w.WriteHeader(tt.infoStatusCode)
|
||||
|
||||
if tt.infoStatusCode == http.StatusOK {
|
||||
responseBytes, err := json.Marshal(tt.infoResponse)
|
||||
require.NoError(t, err)
|
||||
w.Write(responseBytes)
|
||||
}
|
||||
}))
|
||||
defer infoServer.Close()
|
||||
|
||||
var findServer *httptest.Server
|
||||
if tt.findResponse != nil || tt.findStatusCode != 0 {
|
||||
findServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, defaultSeries, r.Header.Get("Snap-Device-Series"))
|
||||
assert.Equal(t, tt.snapID.Name, r.URL.Query().Get("name-startswith"))
|
||||
|
||||
statusCode := tt.findStatusCode
|
||||
if statusCode == 0 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if tt.findResponse != nil && statusCode == http.StatusOK {
|
||||
responseBytes, err := json.Marshal(tt.findResponse)
|
||||
require.NoError(t, err)
|
||||
w.Write(responseBytes)
|
||||
}
|
||||
}))
|
||||
defer findServer.Close()
|
||||
}
|
||||
|
||||
client := &snapcraftClient{
|
||||
InfoAPIURL: infoServer.URL + "/",
|
||||
HTTPClient: &http.Client{},
|
||||
}
|
||||
if findServer != nil {
|
||||
client.FindAPIURL = findServer.URL
|
||||
}
|
||||
|
||||
url, err := client.GetSnapDownloadURL(tt.snapID)
|
||||
tt.expectError(t, err)
|
||||
if err != nil {
|
||||
if tt.errorContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expectedURL, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapcraftClient_GetSnapDownloadURL_InvalidJSON(t *testing.T) {
|
||||
infoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user