Files
s1d3sw1ped 1cc94f2c99
Format / gofmt (push) Failing after 27s
CI / Build (push) Successful in 51s
CI / Go Tests (push) Failing after 21s
initial commit
2026-06-06 07:54:44 -05:00

454 lines
14 KiB
Go

package main
import (
"bytes"
"net/http"
"testing"
"helix-proxy/internal/store"
)
func apiPutRaw(t *testing.T, url, rawBody, token string) *http.Response {
t.Helper()
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader([]byte(rawBody)))
if err != nil {
t.Fatalf("request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("do: %v", err)
}
return resp
}
func TestAPI_UnauthorizedEndpoints(t *testing.T) {
env := newAPITestServer(t)
paths := []string{
"/api/proxy-hosts",
"/api/access-lists",
"/api/streams",
"/api/certificates",
"/api/redirection-hosts",
"/api/dead-hosts",
"/api/settings",
"/api/audit-logs",
"/api/dashboard",
"/api/users",
"/api/users/me",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
assertStatus(t, apiGet(t, env.BaseURL+path, ""), http.StatusUnauthorized)
})
}
}
func TestAPI_BadIDBranches(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
cases := []struct {
name string
method string
path string
want int
}{
{"proxy-host get", http.MethodGet, "/api/proxy-hosts/nope", http.StatusBadRequest},
{"proxy-host put", http.MethodPut, "/api/proxy-hosts/nope", http.StatusBadRequest},
{"proxy-host delete", http.MethodDelete, "/api/proxy-hosts/bad", http.StatusBadRequest},
{"proxy-host enable", http.MethodPost, "/api/proxy-hosts/x/enable", http.StatusBadRequest},
{"proxy-host disable", http.MethodPost, "/api/proxy-hosts/x/disable", http.StatusBadRequest},
{"access-list put", http.MethodPut, "/api/access-lists/abc", http.StatusBadRequest},
{"access-list delete", http.MethodDelete, "/api/access-lists/abc", http.StatusBadRequest},
{"stream put", http.MethodPut, "/api/streams/nope", http.StatusBadRequest},
{"stream delete", http.MethodDelete, "/api/streams/bad", http.StatusBadRequest},
{"stream enable", http.MethodPost, "/api/streams/x/enable", http.StatusBadRequest},
{"stream disable", http.MethodPost, "/api/streams/x/disable", http.StatusBadRequest},
{"cert delete", http.MethodDelete, "/api/certificates/bad", http.StatusBadRequest},
{"redir put", http.MethodPut, "/api/redirection-hosts/nope", http.StatusBadRequest},
{"redir delete", http.MethodDelete, "/api/redirection-hosts/bad", http.StatusBadRequest},
{"redir disable", http.MethodPost, "/api/redirection-hosts/x/disable", http.StatusBadRequest},
{"dead put", http.MethodPut, "/api/dead-hosts/nope", http.StatusBadRequest},
{"dead delete", http.MethodDelete, "/api/dead-hosts/bad", http.StatusBadRequest},
{"dead disable", http.MethodPost, "/api/dead-hosts/x/disable", http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var resp *http.Response
switch tc.method {
case http.MethodGet:
resp = apiGet(t, env.BaseURL+tc.path, token)
case http.MethodPut:
resp = apiPut(t, env.BaseURL+tc.path, map[string]any{"name": "x"}, token)
case http.MethodDelete:
resp = apiDelete(t, env.BaseURL+tc.path, token)
default:
resp = apiPost(t, env.BaseURL+tc.path, nil, token)
}
assertStatus(t, resp, tc.want)
})
}
}
func TestAPI_NotFoundEnableDisable(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
cases := []string{
"/api/proxy-hosts/9999/disable",
"/api/streams/9999/enable",
"/api/streams/9999/disable",
"/api/redirection-hosts/9999/enable",
"/api/dead-hosts/9999/disable",
"/api/dead-hosts/9999/enable",
}
for _, path := range cases {
t.Run(path, func(t *testing.T) {
assertStatus(t, apiPost(t, env.BaseURL+path, nil, token), http.StatusNotFound)
})
}
}
func TestAPI_MalformedJSONOnCreateAndUpdate(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
createPaths := []string{
"/api/proxy-hosts",
"/api/access-lists",
"/api/streams",
"/api/redirection-hosts",
"/api/dead-hosts",
}
for _, path := range createPaths {
t.Run("post "+path, func(t *testing.T) {
assertStatus(t, apiPostRaw(t, env.BaseURL+path, `{`, token), http.StatusBadRequest)
})
}
ph := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"malformed-ph.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 1,
"forwardScheme": "http",
}, token)
if ph.StatusCode != http.StatusCreated {
t.Fatalf("proxy host: %d", ph.StatusCode)
}
var proxyHost store.ProxyHost
decodeJSON(t, ph, &proxyHost)
al := apiPost(t, env.BaseURL+"/api/access-lists", map[string]any{
"name": "malformed-acl",
"clients": []map[string]string{{"address": "all", "directive": "allow"}},
}, token)
if al.StatusCode != http.StatusCreated {
t.Fatalf("acl: %d", al.StatusCode)
}
var accessList store.AccessList
decodeJSON(t, al, &accessList)
strm := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
"incomingPort": pickFreePort(t),
"forwardingHost": "127.0.0.1",
"forwardingPort": 9,
"tcpForwarding": true,
"enabled": false,
}, token)
if strm.StatusCode != http.StatusCreated {
t.Fatalf("stream: %d", strm.StatusCode)
}
var stream store.Stream
decodeJSON(t, strm, &stream)
redir := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
"domainNames": []string{"malformed-redir.example"},
"forwardDomainName": "target.example",
"enabled": true,
}, token)
if redir.StatusCode != http.StatusCreated {
t.Fatalf("redir: %d", redir.StatusCode)
}
var redirection store.RedirectionHost
decodeJSON(t, redir, &redirection)
dead := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
"domainNames": []string{"malformed-dead.example"},
"enabled": true,
}, token)
if dead.StatusCode != http.StatusCreated {
t.Fatalf("dead: %d", dead.StatusCode)
}
var deadHost store.DeadHost
decodeJSON(t, dead, &deadHost)
putCases := []struct {
name string
url string
}{
{"proxy-host", env.BaseURL + "/api/proxy-hosts/" + itoa(proxyHost.ID)},
{"access-list", env.BaseURL + "/api/access-lists/" + itoa(accessList.ID)},
{"stream", env.BaseURL + "/api/streams/" + itoa(stream.ID)},
{"redirection", env.BaseURL + "/api/redirection-hosts/" + itoa(redirection.ID)},
{"dead-host", env.BaseURL + "/api/dead-hosts/" + itoa(deadHost.ID)},
}
for _, tc := range putCases {
t.Run("put "+tc.name, func(t *testing.T) {
assertStatus(t, apiPutRaw(t, tc.url, `{`, token), http.StatusBadRequest)
})
}
}
func TestAPI_UpdateConflict409(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
t.Run("proxy-hosts", func(t *testing.T) {
a := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"conflict-a.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 1,
"forwardScheme": "http",
}, token)
b := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"conflict-b.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 2,
"forwardScheme": "http",
}, token)
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
}
var hostB store.ProxyHost
decodeJSON(t, b, &hostB)
assertStatus(t, apiPut(t, env.BaseURL+"/api/proxy-hosts/"+itoa(hostB.ID), map[string]any{
"domainNames": []string{"conflict-a.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 2,
"forwardScheme": "http",
}, token), http.StatusConflict)
})
t.Run("redirection-hosts", func(t *testing.T) {
a := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
"domainNames": []string{"conflict-redir-a.example"},
"forwardDomainName": "a.example",
"enabled": true,
}, token)
b := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
"domainNames": []string{"conflict-redir-b.example"},
"forwardDomainName": "b.example",
"enabled": true,
}, token)
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
}
var hostB store.RedirectionHost
decodeJSON(t, b, &hostB)
assertStatus(t, apiPut(t, env.BaseURL+"/api/redirection-hosts/"+itoa(hostB.ID), map[string]any{
"domainNames": []string{"conflict-redir-a.example"},
"forwardDomainName": "b.example",
"enabled": true,
}, token), http.StatusConflict)
})
t.Run("dead-hosts", func(t *testing.T) {
a := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
"domainNames": []string{"conflict-dead-a.example"},
"enabled": true,
}, token)
b := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
"domainNames": []string{"conflict-dead-b.example"},
"enabled": true,
}, token)
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
}
var hostB store.DeadHost
decodeJSON(t, b, &hostB)
assertStatus(t, apiPut(t, env.BaseURL+"/api/dead-hosts/"+itoa(hostB.ID), map[string]any{
"domainNames": []string{"conflict-dead-a.example"},
"enabled": true,
}, token), http.StatusConflict)
})
t.Run("streams", func(t *testing.T) {
port := pickFreePort(t)
a := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
"incomingPort": port,
"forwardingHost": "127.0.0.1",
"forwardingPort": 9,
"tcpForwarding": true,
"enabled": false,
}, token)
otherPort := pickFreePort(t)
b := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
"incomingPort": otherPort,
"forwardingHost": "127.0.0.1",
"forwardingPort": 10,
"tcpForwarding": true,
"enabled": false,
}, token)
if a.StatusCode != http.StatusCreated || b.StatusCode != http.StatusCreated {
t.Fatalf("create: %d %d", a.StatusCode, b.StatusCode)
}
var streamB store.Stream
decodeJSON(t, b, &streamB)
assertStatus(t, apiPut(t, env.BaseURL+"/api/streams/"+itoa(streamB.ID), map[string]any{
"incomingPort": port,
"forwardingHost": "127.0.0.1",
"forwardingPort": 10,
"tcpForwarding": true,
"enabled": false,
}, token), http.StatusConflict)
})
}
func TestAPI_ForbiddenEnableDisableBranches(t *testing.T) {
env := newAPITestServer(t)
adminTok := env.loginToken(t)
userTok := env.userToken(t, 2)
ph := apiPost(t, env.BaseURL+"/api/proxy-hosts", map[string]any{
"domainNames": []string{"forbid-enable-ph.example"},
"forwardHost": "127.0.0.1",
"forwardPort": 1,
"forwardScheme": "http",
"enabled": true,
}, adminTok)
var proxyHost store.ProxyHost
decodeJSON(t, ph, &proxyHost)
assertStatus(t, apiPost(t, env.BaseURL+"/api/proxy-hosts/"+itoa(proxyHost.ID)+"/enable", nil, userTok), http.StatusForbidden)
strm := apiPost(t, env.BaseURL+"/api/streams", map[string]any{
"incomingPort": pickFreePort(t),
"forwardingHost": "127.0.0.1",
"forwardingPort": 9,
"tcpForwarding": true,
"enabled": false,
}, adminTok)
var stream store.Stream
decodeJSON(t, strm, &stream)
assertStatus(t, apiPost(t, env.BaseURL+"/api/streams/"+itoa(stream.ID)+"/disable", nil, userTok), http.StatusForbidden)
redir := apiPost(t, env.BaseURL+"/api/redirection-hosts", map[string]any{
"domainNames": []string{"forbid-disable-redir.example"},
"forwardDomainName": "x.example",
"enabled": true,
}, adminTok)
var redirection store.RedirectionHost
decodeJSON(t, redir, &redirection)
assertStatus(t, apiPost(t, env.BaseURL+"/api/redirection-hosts/"+itoa(redirection.ID)+"/disable", nil, userTok), http.StatusForbidden)
dead := apiPost(t, env.BaseURL+"/api/dead-hosts", map[string]any{
"domainNames": []string{"forbid-enable-dead.example"},
"enabled": true,
}, adminTok)
var deadHost store.DeadHost
decodeJSON(t, dead, &deadHost)
assertStatus(t, apiPost(t, env.BaseURL+"/api/dead-hosts/"+itoa(deadHost.ID)+"/enable", nil, userTok), http.StatusForbidden)
}
func TestAPI_LoginLegacyPlainPassword(t *testing.T) {
env := newAPITestServer(t)
u, ok := env.Store.GetUserByEmail("admin@example.com")
if !ok {
t.Fatal("admin user missing")
}
u.Password = "legacyplain"
if err := env.Store.UpdateUser(u); err != nil {
t.Fatalf("update user: %v", err)
}
resp := apiPost(t, env.BaseURL+"/api/login", map[string]any{"password": "legacyplain"}, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("legacy login: %d %s", resp.StatusCode, readBody(resp))
}
var loginOut struct {
Token string `json:"token"`
}
decodeJSON(t, resp, &loginOut)
if loginOut.Token == "" {
t.Fatal("empty token for legacy login")
}
assertStatus(t, apiPost(t, env.BaseURL+"/api/users/me/password", map[string]any{
"currentPassword": "legacyplain",
"newPassword": "newsecure123",
}, loginOut.Token), http.StatusOK)
}
func TestAPI_PasswordChangeEmptyNew(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
assertStatus(t, apiPost(t, env.BaseURL+"/api/users/me/password", map[string]any{
"currentPassword": "password",
"newPassword": "",
}, token), http.StatusBadRequest)
}
func TestAPI_ListEndpointsOK(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
lists := []string{
"/api/access-lists",
"/api/streams",
"/api/certificates",
"/api/redirection-hosts",
"/api/dead-hosts",
"/api/settings",
}
for _, path := range lists {
t.Run(path, func(t *testing.T) {
assertStatus(t, apiGet(t, env.BaseURL+path, token), http.StatusOK)
})
}
}
func TestAPI_DeleteNotFoundBranches(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
cases := []string{
"/api/redirection-hosts/9999",
"/api/dead-hosts/9999",
"/api/streams/9999",
}
for _, path := range cases {
t.Run(path, func(t *testing.T) {
assertStatus(t, apiDelete(t, env.BaseURL+path, token), http.StatusNotFound)
})
}
}
func TestAPI_PutNotFoundBranches(t *testing.T) {
env := newAPITestServer(t)
token := env.loginToken(t)
cases := []struct {
path string
body map[string]any
}{
{"/api/streams/9999", map[string]any{
"incomingPort": 1, "forwardingHost": "127.0.0.1", "forwardingPort": 1, "tcpForwarding": true,
}},
{"/api/redirection-hosts/9999", map[string]any{"domainNames": []string{"x.example"}}},
{"/api/dead-hosts/9999", map[string]any{"domainNames": []string{"x.example"}}},
}
for _, tc := range cases {
t.Run(tc.path, func(t *testing.T) {
assertStatus(t, apiPut(t, env.BaseURL+tc.path, tc.body, token), http.StatusNotFound)
})
}
}