Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,11 @@ func (c *Client) EndpointAuthPoll(ctx context.Context, pollCode string) (*messag
return d, err
}

func (c *Client) Downloads(ctx context.Context) (*message.DownloadsData, error) {
_, d, err := callAPI[message.DownloadsData](ctx, c, "GET", message.DownloadsEndpoint, nil)
return d, err
}

func urlPath(base, path string) (string, error) {
baseURL, err := url.Parse(base)
if err != nil {
Expand Down
75 changes: 75 additions & 0 deletions client_fixtures_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dnapi

const downloadsResponse = `{
"data": {
"dnclient": {
"0.8.4": {
"freebsd-amd64": "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/amd64/dnclient",
"freebsd-arm64": "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/arm64/dnclient",
"linux-386": "https://dl.defined.net/290ff4b6/v0.8.4/linux/386/dnclient",
"linux-amd64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/amd64/dnclient",
"linux-arm64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm64/dnclient",
"linux-armv5": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-5/dnclient",
"linux-armv6": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-6/dnclient",
"linux-armv7": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-7/dnclient",
"linux-mips": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips/dnclient",
"linux-mips-softfloat": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips-softfloat/dnclient",
"linux-mips64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips64/dnclient",
"linux-mips64le": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips64le/dnclient",
"linux-mipsle": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mipsle/dnclient",
"linux-ppc64le": "https://dl.defined.net/290ff4b6/v0.8.4/linux/ppc64le/dnclient",
"linux-riscv64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/riscv64/dnclient",
"macos-universal-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Desktop.dmg",
"macos-universal-server": "https://dl.defined.net/290ff4b6/v0.8.4/macos/dnclient",
"macos-universal-server-dmg": "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Server.dmg",
"windows-amd64-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Desktop.msi",
"windows-amd64-server": "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Server.msi",
"windows-arm64-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Desktop.msi",
"windows-arm64-server": "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Server.msi"
},
"latest": {
"freebsd-amd64": "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/amd64/dnclient",
"freebsd-arm64": "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/arm64/dnclient",
"linux-386": "https://dl.defined.net/290ff4b6/v0.8.4/linux/386/dnclient",
"linux-amd64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/amd64/dnclient",
"linux-arm64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm64/dnclient",
"linux-armv5": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-5/dnclient",
"linux-armv6": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-6/dnclient",
"linux-armv7": "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm-7/dnclient",
"linux-mips": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips/dnclient",
"linux-mips-softfloat": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips-softfloat/dnclient",
"linux-mips64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips64/dnclient",
"linux-mips64le": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mips64le/dnclient",
"linux-mipsle": "https://dl.defined.net/290ff4b6/v0.8.4/linux/mipsle/dnclient",
"linux-ppc64le": "https://dl.defined.net/290ff4b6/v0.8.4/linux/ppc64le/dnclient",
"linux-riscv64": "https://dl.defined.net/290ff4b6/v0.8.4/linux/riscv64/dnclient",
"macos-universal-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Desktop.dmg",
"macos-universal-server": "https://dl.defined.net/290ff4b6/v0.8.4/macos/dnclient",
"macos-universal-server-dmg": "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Server.dmg",
"windows-amd64-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Desktop.msi",
"windows-amd64-server": "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Server.msi",
"windows-arm64-desktop": "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Desktop.msi",
"windows-arm64-server": "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Server.msi"
}
},
"mobile": {
"android": "https://play.google.com/store/apps/details?id=net.defined.mobile_nebula",
"ios": "https://apps.apple.com/us/app/mobile-nebula/id1509587936"
},
"container": {
"docker": "https://hub.docker.com/r/definednet/dnclient/"
},
"versionInfo": {
"dnclient": {
"0.8.4": {
"latest": true,
"releaseDate": "2025-10-10"
}
},
"latest": {
"dnclient": "0.8.4",
"mobile": "0.5.1"
}
}
}
}`
56 changes: 56 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,3 +1156,59 @@ func TestDoOidcPoll(t *testing.T) {
assert.Empty(t, ts.Errors())
assert.Equal(t, 0, ts.RequestsRemaining())
}

func TestDownloads(t *testing.T) {
t.Parallel()

useragent := "dnclientUnitTests/1.0.0 (not a real client)"
ts := dnapitest.NewServer(useragent)
client := NewClient(useragent, ts.URL)
t.Cleanup(func() { ts.Close() })

// Happy path - successful downloads response
ts.ExpectAPIRequest(http.StatusOK, func(r any) []byte {
return []byte(downloadsResponse)
})

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): You can use https://pkg.go.dev/testing#T.Context now, I think? should allow tying the context to the runtime of the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd approve a pr to update the repo with this pattern, but will leave this as-is to match the rest of the file.

defer cancel()
resp, err := client.Downloads(ctx)
require.NoError(t, err)
assert.Empty(t, ts.Errors())
assert.Equal(t, 0, ts.RequestsRemaining())

// Verify DNClient downloads - this list is not exhaustive but tests some of the more common platforms.
require.NotNil(t, resp)
require.Contains(t, resp.DNClient, "0.8.4")
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/amd64/dnclient", resp.DNClient["0.8.4"]["freebsd-amd64"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/freebsd/arm64/dnclient", resp.DNClient["0.8.4"]["freebsd-arm64"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/linux/amd64/dnclient", resp.DNClient["0.8.4"]["linux-amd64"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/linux/arm64/dnclient", resp.DNClient["0.8.4"]["linux-arm64"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Desktop.msi", resp.DNClient["0.8.4"]["windows-amd64-desktop"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/windows/amd64/DNClient-Server.msi", resp.DNClient["0.8.4"]["windows-amd64-server"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Desktop.msi", resp.DNClient["0.8.4"]["windows-arm64-desktop"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/windows/arm64/DNClient-Server.msi", resp.DNClient["0.8.4"]["windows-arm64-server"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/macos/dnclient", resp.DNClient["0.8.4"]["macos-universal-server"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Server.dmg", resp.DNClient["0.8.4"]["macos-universal-server-dmg"])
assert.Equal(t, "https://dl.defined.net/290ff4b6/v0.8.4/macos/DNClient-Desktop.dmg", resp.DNClient["0.8.4"]["macos-universal-desktop"])

// Verify the latest release info
require.Contains(t, resp.DNClient, "latest")
require.Equal(t, resp.DNClient["latest"], resp.DNClient["0.8.4"])

// Verify mobile downloads
assert.Equal(t, "https://play.google.com/store/apps/details?id=net.defined.mobile_nebula", resp.Mobile.Android)
assert.Equal(t, "https://apps.apple.com/us/app/mobile-nebula/id1509587936", resp.Mobile.IOS)

// Verify container downloads
assert.Equal(t, "https://hub.docker.com/r/definednet/dnclient/", resp.Container.Docker)

// Verify version info
require.Contains(t, resp.VersionInfo.DNClient, "0.8.4")
assert.True(t, resp.VersionInfo.DNClient["0.8.4"].Latest)
assert.Equal(t, "2025-10-10", resp.VersionInfo.DNClient["0.8.4"].ReleaseDate)

// Verify latest versions
assert.Equal(t, "0.8.4", resp.VersionInfo.Latest.DNClient)
assert.Equal(t, "0.5.1", resp.VersionInfo.Latest.Mobile)
}
5 changes: 5 additions & 0 deletions dnapitest/dnapitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(expected.Respond(nil))
case message.AuthPollEndpoint:
s.handlerDoOidcPoll(w, r)
case message.DownloadsEndpoint:
expected := s.expectedRequests[0]
s.expectedRequests = s.expectedRequests[1:]
w.WriteHeader(expected.StatusCode())
_, _ = w.Write(expected.Respond(nil))
default:
s.errors = append(s.errors, fmt.Errorf("invalid request path %s", r.URL.Path))
http.NotFound(w, r)
Expand Down
40 changes: 40 additions & 0 deletions message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,46 @@ type EndpointAuthPollData struct {
EnrollmentCode string `json:"enrollmentCode"`
}

const DownloadsEndpoint = "/v1/downloads"

type DownloadsData struct {
// DNClient maps versions to a map of platforms' download links.
DNClient map[string]map[string]string `json:"dnclient"`
// Mobile maps platforms to their download links (i.e. App Store / Play Store.)
Mobile DownloadsMobile `json:"mobile"`
// Links to container repositories like Docker
Container DownloadsContainers `json:"container"`

// VersionInfo contains information about past versions.
VersionInfo DownloadsVersionInfo `json:"versionInfo"`
}

type DownloadsVersionInfo struct {
// DNClient maps versions to their version info.
DNClient map[string]DNClientVersionInfo `json:"dnclient"`
// Latest returns the latest versions for each platform.
Latest DownloadsLatest `json:"latest"`
}

type DownloadsMobile struct {
Android string `json:"android"`
IOS string `json:"ios"`
}

type DownloadsContainers struct {
Docker string `json:"docker"`
}

type DownloadsLatest struct {
DNClient string `json:"dnclient"`
Mobile string `json:"mobile"`
}

type DNClientVersionInfo struct {
Latest bool `json:"latest"`
ReleaseDate string `json:"releaseDate"`
}

// NetworkCurve represents the network curve specified by the API.
type NetworkCurve string

Expand Down
Loading