mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
small fix, don't worry about it
This commit is contained in:
1 parent
08bd0c6e5c
commit
08cb5754f3
638 files changed
+57330
-34704
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"containr/internal/apwhy/config"
|
||||
"containr/internal/apwhy/storage"
|
||||
)
|
||||
|
||||
type apiErrorPayload struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type apiResponsePayload struct {
|
||||
OK bool `json:"ok"`
|
||||
Data any `json:"data"`
|
||||
Error apiErrorPayload `json:"error"`
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) (*httptest.Server, *http.Client) {
|
||||
t.Helper()
|
||||
|
||||
cfg := config.Load()
|
||||
cfg.SQLitePath = ":memory:"
|
||||
cfg.CookieSecure = false
|
||||
|
||||
store, err := storage.Open(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test storage: %v", err)
|
||||
}
|
||||
|
||||
server := NewServer(store, cfg)
|
||||
ts := httptest.NewServer(server.Handler())
|
||||
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
_ = store.Close()
|
||||
})
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cookie jar: %v", err)
|
||||
}
|
||||
|
||||
return ts, &http.Client{Jar: jar}
|
||||
}
|
||||
|
||||
func requestJSON(t *testing.T, client *http.Client, method string, url string, body string) (int, apiResponsePayload) {
|
||||
t.Helper()
|
||||
|
||||
var bodyReader *bytes.Reader
|
||||
if body == "" {
|
||||
bodyReader = bytes.NewReader(nil)
|
||||
} else {
|
||||
bodyReader = bytes.NewReader([]byte(body))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
if body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var payload apiResponsePayload
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode response body: %v", err)
|
||||
}
|
||||
|
||||
return resp.StatusCode, payload
|
||||
}
|
||||
|
||||
func bootstrapAndLoginOwner(t *testing.T, client *http.Client, baseURL string) {
|
||||
t.Helper()
|
||||
|
||||
status, payload := requestJSON(
|
||||
t,
|
||||
client,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/bootstrap/register-owner",
|
||||
`{"email":"[email protected]","password":"owner-pass-123"}`,
|
||||
)
|
||||
if status != http.StatusCreated || !payload.OK {
|
||||
t.Fatalf("owner bootstrap failed: status=%d payload=%+v", status, payload)
|
||||
}
|
||||
|
||||
status, payload = requestJSON(
|
||||
t,
|
||||
client,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/auth/login",
|
||||
`{"email":"[email protected]","password":"owner-pass-123"}`,
|
||||
)
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("owner login failed: status=%d payload=%+v", status, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerBootstrapLoginAndProtectedRoutes(t *testing.T) {
|
||||
ts, authClient := newTestServer(t)
|
||||
baseURL := ts.URL
|
||||
|
||||
// Bootstrap should be open before any user exists.
|
||||
status, payload := requestJSON(t, authClient, http.MethodGet, baseURL+"/api/v1/bootstrap/status", "")
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("unexpected bootstrap status response: status=%d payload=%+v", status, payload)
|
||||
}
|
||||
bootstrapData, ok := payload.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload for bootstrap status, got %#v", payload.Data)
|
||||
}
|
||||
if hasUsers, _ := bootstrapData["hasUsers"].(bool); hasUsers {
|
||||
t.Fatalf("expected hasUsers=false before owner bootstrap")
|
||||
}
|
||||
if registrationOpen, _ := bootstrapData["registrationOpen"].(bool); !registrationOpen {
|
||||
t.Fatalf("expected registrationOpen=true before owner bootstrap")
|
||||
}
|
||||
|
||||
// Protected route without auth should fail.
|
||||
anonClient := &http.Client{}
|
||||
status, payload = requestJSON(t, anonClient, http.MethodGet, baseURL+"/api/v1/auth/me", "")
|
||||
if status != http.StatusUnauthorized || payload.OK {
|
||||
t.Fatalf("expected unauthorized /auth/me response, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
|
||||
// Register owner and login.
|
||||
bootstrapAndLoginOwner(t, authClient, baseURL)
|
||||
|
||||
// Bootstrap should close once owner exists.
|
||||
status, payload = requestJSON(t, authClient, http.MethodGet, baseURL+"/api/v1/bootstrap/status", "")
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("unexpected bootstrap status after owner creation: status=%d payload=%+v", status, payload)
|
||||
}
|
||||
bootstrapData, ok = payload.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload for bootstrap status, got %#v", payload.Data)
|
||||
}
|
||||
if hasUsers, _ := bootstrapData["hasUsers"].(bool); !hasUsers {
|
||||
t.Fatalf("expected hasUsers=true after owner bootstrap")
|
||||
}
|
||||
if registrationOpen, _ := bootstrapData["registrationOpen"].(bool); registrationOpen {
|
||||
t.Fatalf("expected registrationOpen=false after owner bootstrap")
|
||||
}
|
||||
|
||||
// Owner session cookie should authorize /auth/me.
|
||||
status, payload = requestJSON(t, authClient, http.MethodGet, baseURL+"/api/v1/auth/me", "")
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("expected successful /auth/me response, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
meData, ok := payload.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload for auth/me, got %#v", payload.Data)
|
||||
}
|
||||
if email, _ := meData["email"].(string); email != "[email protected]" {
|
||||
t.Fatalf("unexpected /auth/me email: %q", email)
|
||||
}
|
||||
|
||||
// Logout should revoke auth session.
|
||||
status, payload = requestJSON(t, authClient, http.MethodPost, baseURL+"/api/v1/auth/logout", "")
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("expected successful logout response, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
status, payload = requestJSON(t, authClient, http.MethodGet, baseURL+"/api/v1/auth/me", "")
|
||||
if status != http.StatusUnauthorized || payload.OK {
|
||||
t.Fatalf("expected unauthorized /auth/me after logout, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerServicesCreateValidationRules(t *testing.T) {
|
||||
ts, client := newTestServer(t)
|
||||
baseURL := ts.URL
|
||||
|
||||
bootstrapAndLoginOwner(t, client, baseURL)
|
||||
|
||||
// Route prefix under /api must be rejected.
|
||||
status, payload := requestJSON(
|
||||
t,
|
||||
client,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/services",
|
||||
`{"name":"Bad Service","upstreamUrl":"http://example.com","routePrefix":"/api/conflict"}`,
|
||||
)
|
||||
if status != http.StatusBadRequest || payload.OK {
|
||||
t.Fatalf("expected route conflict validation failure, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
if payload.Error.Code != "ROUTE_CONFLICT" {
|
||||
t.Fatalf("expected ROUTE_CONFLICT error code, got %q", payload.Error.Code)
|
||||
}
|
||||
|
||||
// Valid service should be created successfully.
|
||||
status, payload = requestJSON(
|
||||
t,
|
||||
client,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/services",
|
||||
`{"name":"Web Frontend","upstreamUrl":"http://example.com","routePrefix":"/web"}`,
|
||||
)
|
||||
if status != http.StatusCreated || !payload.OK {
|
||||
t.Fatalf("expected successful service creation, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
createData, ok := payload.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload for service creation, got %#v", payload.Data)
|
||||
}
|
||||
if _, ok := createData["id"].(string); !ok {
|
||||
t.Fatalf("expected created service id in response")
|
||||
}
|
||||
if slug, _ := createData["slug"].(string); !strings.Contains(slug, "web-frontend") {
|
||||
t.Fatalf("expected normalized service slug, got %q", slug)
|
||||
}
|
||||
|
||||
// List should contain the newly created service.
|
||||
status, payload = requestJSON(t, client, http.MethodGet, baseURL+"/api/v1/services", "")
|
||||
if status != http.StatusOK || !payload.OK {
|
||||
t.Fatalf("expected successful services list response, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
rows, ok := payload.Data.([]any)
|
||||
if !ok || len(rows) == 0 {
|
||||
t.Fatalf("expected non-empty services list, got %#v", payload.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerProxyWithAPIKey(t *testing.T) {
|
||||
ts, authClient := newTestServer(t)
|
||||
baseURL := ts.URL
|
||||
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("x-api-key") != "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = io.WriteString(w, "unexpected-api-key-header")
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, "ok:"+r.URL.Path+"?"+r.URL.RawQuery)
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
bootstrapAndLoginOwner(t, authClient, baseURL)
|
||||
|
||||
// Create service exposed at /web/* and proxied to the upstream test server.
|
||||
status, payload := requestJSON(
|
||||
t,
|
||||
authClient,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/services",
|
||||
`{"name":"Web Proxy","upstreamUrl":"`+upstream.URL+`","routePrefix":"/web"}`,
|
||||
)
|
||||
if status != http.StatusCreated || !payload.OK {
|
||||
t.Fatalf("expected successful service creation, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
|
||||
// Mint API key for gateway access.
|
||||
status, payload = requestJSON(
|
||||
t,
|
||||
authClient,
|
||||
http.MethodPost,
|
||||
baseURL+"/api/v1/keys",
|
||||
`{"name":"integration-key","plan":"free"}`,
|
||||
)
|
||||
if status != http.StatusCreated || !payload.OK {
|
||||
t.Fatalf("expected successful key creation, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
keyData, ok := payload.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload for key creation, got %#v", payload.Data)
|
||||
}
|
||||
rawKey, _ := keyData["key"].(string)
|
||||
if strings.TrimSpace(rawKey) == "" {
|
||||
t.Fatalf("expected non-empty raw API key")
|
||||
}
|
||||
|
||||
// No API key should be rejected.
|
||||
anonClient := &http.Client{}
|
||||
status, payload = requestJSON(t, anonClient, http.MethodGet, baseURL+"/web/ping?x=1", "")
|
||||
if status != http.StatusUnauthorized || payload.OK {
|
||||
t.Fatalf("expected unauthorized proxy request without key, got status=%d payload=%+v", status, payload)
|
||||
}
|
||||
if payload.Error.Code != "API_KEY_MISSING" {
|
||||
t.Fatalf("expected API_KEY_MISSING error code, got %q", payload.Error.Code)
|
||||
}
|
||||
|
||||
// Valid API key should proxy request and strip the key before forwarding.
|
||||
req, err := http.NewRequest(http.MethodGet, baseURL+"/web/ping?x=1", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create proxy request: %v", err)
|
||||
}
|
||||
req.Header.Set("x-api-key", rawKey)
|
||||
|
||||
resp, err := anonClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("proxy request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read proxy response body: %v", err)
|
||||
}
|
||||
body := string(bodyBytes)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected proxy status 200, got %d body=%q", resp.StatusCode, body)
|
||||
}
|
||||
if body != "ok:/ping?x=1" {
|
||||
t.Fatalf("unexpected proxy body: %q", body)
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
@font-face{font-family:Manrope;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,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) format("woff2"),url(data:font/woff;base64,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 truncated
|
||||
@@ -0,0 +1 @@
|
||||
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))n(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(s){if(s.ep)return;s.ep=!0;const i=r(s);fetch(s.href,i)}})();const Ge=!1,We=(e,t)=>e===t,Se=Symbol("solid-proxy"),Qe=typeof Proxy=="function",Xe=Symbol("solid-track"),de={equals:We};let je=Fe;const J=1,fe=2,Ve={owned:null,cleanups:null,context:null,owner:null},ve={};var S=null;let xe=null,Ye=null,_=null,I=null,q=null,he=0;function ce(e,t){const r=_,n=S,s=e.length===0,i=t===void 0?n:t,o=s?Ve:{owned:null,cleanups:null,context:i?i.context:null,owner:i},a=s?e:()=>e(()=>j(()=>re(o)));S=o,_=null;try{return H(a,!0)}finally{_=r,S=n}}function y(e,t){t=t?Object.assign({},de,t):de;const r={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},n=s=>(typeof s=="function"&&(s=s(r.value)),qe(r,s));return[Be.bind(r),n]}function ze(e,t,r){const n=me(e,t,!0,J);z(n)}function L(e,t,r){const n=me(e,t,!1,J);z(n)}function Ee(e,t,r){je=it;const n=me(e,t,!1,J);n.user=!0,q?q.push(n):z(n)}function R(e,t,r){r=r?Object.assign({},de,r):de;const n=me(e,t,!0,0);return n.observers=null,n.observerSlots=null,n.comparator=r.equals||void 0,z(n),Be.bind(n)}function Ze(e){return e&&typeof e=="object"&&"then"in e}function F(e,t,r){let n,s,i;n=!0,s=e,i={};let o=null,a=ve,h=!1,m="initialValue"in i,d=typeof n=="function"&&R(n);const l=new Set,[f,g]=(i.storage||y)(i.initialValue),[b,x]=y(void 0),[V,U]=y(void 0,{equals:!1}),[T,O]=y(m?"ready":"unresolved");function k(w,$,B,N){return o===w&&(o=null,N!==void 0&&(m=!0),(w===a||$===a)&&i.onHydrated&&queueMicrotask(()=>i.onHydrated(N,{value:$})),a=ve,M($,B)),$}function M(w,$){H(()=>{$===void 0&&g(()=>w),O($!==void 0?"errored":m?"ready":"unresolved"),x($);for(const B of l.keys())B.decrement();l.clear()},!1)}function G(){const w=nt,$=f(),B=b();if(B!==void 0&&!o)throw B;return _&&_.user,$}function Z(w=!0){if(w!==!1&&h)return;h=!1;const $=d?d():n;if($==null||$===!1){k(o,j(f));return}let B;const N=a!==ve?a:j(()=>{try{return s($,{value:f(),refetching:w})}catch(te){B=te}});if(B!==void 0){k(o,void 0,ue(B),$);return}else if(!Ze(N))return k(o,N,void 0,$),N;return o=N,"v"in N?(N.s===1?k(o,N.v,void 0,$):k(o,void 0,ue(N.v),$),N):(h=!0,queueMicrotask(()=>h=!1),H(()=>{O(m?"refreshing":"pending"),U()},!1),N.then(te=>k(N,te,void 0,$),te=>k(N,void 0,ue(te),$)))}Object.defineProperties(G,{state:{get:()=>T()},error:{get:()=>b()},loading:{get(){const w=T();return w==="pending"||w==="refreshing"}},latest:{get(){if(!m)return G();const w=b();if(w&&!o)throw w;return f()}}});let ee=S;return d?ze(()=>(ee=S,Z(!1))):Z(!1),[G,{refetch:w=>tt(ee,()=>Z(w)),mutate:g}]}function j(e){if(_===null)return e();const t=_;_=null;try{return e()}finally{_=t}}function et(e){return S===null||(S.cleanups===null?S.cleanups=[e]:S.cleanups.push(e)),e}function tt(e,t){const r=S,n=_;S=e,_=null;try{return H(t,!0)}catch(s){ke(s)}finally{S=r,_=n}}const[rl,nl]=y(!1);function rt(e){const t=R(e),r=R(()=>Ce(t()));return r.toArray=()=>{const n=r();return Array.isArray(n)?n:n!=null?[n]:[]},r}let nt;function Be(){if(this.sources&&this.state)if(this.state===J)z(this);else{const e=I;I=null,H(()=>ge(this),!1),I=e}if(_){const e=this.observers?this.observers.length:0;_.sources?(_.sources.push(this),_.sourceSlots.push(e)):(_.sources=[this],_.sourceSlots=[e]),this.observers?(this.observers.push(_),this.observerSlots.push(_.sources.length-1)):(this.observers=[_],this.observerSlots=[_.sources.length-1])}return this.value}function qe(e,t,r){let n=e.value;return(!e.comparator||!e.comparator(n,t))&&(e.value=t,e.observers&&e.observers.length&&H(()=>{for(let s=0;s<e.observers.length;s+=1){const i=e.observers[s],o=xe&&xe.running;o&&xe.disposed.has(i),(o?!i.tState:!i.state)&&(i.pure?I.push(i):q.push(i),i.observers&&Ke(i)),o||(i.state=J)}if(I.length>1e6)throw I=[],new Error},!1)),t}function z(e){if(!e.fn)return;re(e);const t=he;lt(e,e.value,t)}function lt(e,t,r){let n;const s=S,i=_;_=S=e;try{n=e.fn(t)}catch(o){return e.pure&&(e.state=J,e.owned&&e.owned.forEach(re),e.owned=null),e.updatedAt=r+1,ke(o)}finally{_=i,S=s}(!e.updatedAt||e.updatedAt<=r)&&(e.updatedAt!=null&&"observers"in e?qe(e,n):e.value=n,e.updatedAt=r)}function me(e,t,r,n=J,s){const i={fn:e,state:n,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:S,context:S?S.context:null,pure:r};return S===null||S!==Ve&&(S.owned?S.owned.push(i):S.owned=[i]),i}function pe(e){if(e.state===0)return;if(e.state===fe)return ge(e);if(e.suspense&&j(e.suspenLine truncated
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>APwhy</title>
|
||||
<script type="module" crossorigin src="/assets/index-DwfYiTMH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DRUelTBf.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user