Compare commits

...

1 Commits
V1.0.0 ... main

Author SHA1 Message Date
7cf6eabbdd ci: add integration tests and pipeline step
All checks were successful
build and deploy kleinTodo / build (push) Successful in 51s
build and deploy kleinTodo / build (release) Successful in 1m24s
Co-authored-by: Junie <junie@jetbrains.com>
2026-04-04 13:55:37 +02:00
20 changed files with 453 additions and 39 deletions

View File

@ -28,6 +28,9 @@ jobs:
chmod +x build.sh
./build.sh
- name: Run tests
run: go test -v ./tests/...
- name: Build the Docker image
run: docker build -t gitea.kleinsense.nl/dariusklein/klein_todo:latest -f server/Dockerfile .

View File

@ -3,6 +3,7 @@ package httpClient
import (
"fmt"
"net/http"
"time"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
)
@ -23,7 +24,10 @@ func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// GetHttpClient Returns httpClient with jwt in headers
func getHttpClient(token string) *http.Client {
return &http.Client{Transport: &AuthTransport{Token: token}}
return &http.Client{
Transport: &AuthTransport{Token: token},
Timeout: 30 * time.Second,
}
}
// GetHttpClient Returns CustomClient with jwt in headers

View File

@ -56,6 +56,10 @@ func loginAction(context context.Context, c *cli.Command) error {
var password = c.String(passwordFlagName)
var serverUrl = c.String(serverUrlFlagName)
return LoginAndSave(serverUrl, username, password)
}
func LoginAndSave(serverUrl, username, password string) error {
token, err := loginAndGetToken(serverUrl, username, password)
if err != nil {
return err
@ -72,6 +76,10 @@ func loginAction(context context.Context, c *cli.Command) error {
cfg.Server.Url = serverUrl
}
return SaveConfig(cfg)
}
func SaveConfig(c config.Config) error {
path, configPath, err := config.GetConfigPath()
if err != nil {
return err
@ -79,9 +87,11 @@ func loginAction(context context.Context, c *cli.Command) error {
if err = os.MkdirAll(path, 0770); err != nil {
return err
}
configBytes, err := toml.Marshal(cfg)
err = os.WriteFile(configPath, configBytes, 0644)
return nil
configBytes, err := toml.Marshal(c)
if err != nil {
return err
}
return os.WriteFile(configPath, configBytes, 0644)
}
func loginAndGetToken(url, username, password string) (string, error) {

View File

@ -6,14 +6,10 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
"github.com/BurntSushi/toml"
"github.com/urfave/cli/v3"
)
@ -69,19 +65,7 @@ func registerAction(context context.Context, c *cli.Command) error {
cfg.Server.Url = serverUrl
}
path, configPath, err := config.GetConfigPath()
if err != nil {
return err
}
if err = os.MkdirAll(path, 0770); err != nil {
return err
}
configBytes, err := toml.Marshal(cfg)
err = os.WriteFile(configPath, configBytes, 0644)
log.Println("Registered new user to kleinTodo server successfully")
return nil
return SaveConfig(cfg)
}
func registerAndGetToken(url, username, password string) error {

View File

@ -24,6 +24,17 @@ func Sync() *cli.Command {
// syncAction logic for Template
func syncAction(context context.Context, c *cli.Command) error {
if cfg.Server.Token == "" {
if common.AskUserBool("No valid JWT found. Do you wish to login using your stored credentials?") {
err := LoginAndSave(cfg.Server.Url, cfg.Server.Credentials.Username, cfg.Server.Credentials.Password)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
} else {
return fmt.Errorf("login required to sync")
}
}
store, err := common.GetTodoDataStore()
if err != nil {
return err
@ -46,12 +57,35 @@ func syncAction(context context.Context, c *cli.Command) error {
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.GetHttpClient(cfg.Server.Token).Do(req)
fmt.Println("Sending sync request to server...")
token := cfg.Server.Token
resp, err := httpClient.GetHttpClient(token).Do(req)
if err != nil {
return fmt.Errorf("error sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
if common.AskUserBool("Login expired or invalid. Do you wish to login using your stored credentials?") {
err := LoginAndSave(cfg.Server.Url, cfg.Server.Credentials.Username, cfg.Server.Credentials.Password)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
// Re-send request with new token
req, _ = http.NewRequest("POST", cfg.Server.Url+"/sync", bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
newToken := cfg.Server.Token
resp, err = httpClient.GetHttpClient(newToken).Do(req)
if err != nil {
return fmt.Errorf("error sending request after login: %w", err)
}
defer resp.Body.Close()
} else {
return fmt.Errorf("unauthorized: login required to sync")
}
}
fmt.Println("Reading response body...")
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
@ -61,6 +95,7 @@ func syncAction(context context.Context, c *cli.Command) error {
return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
}
fmt.Println("Decoding sync response...")
var response common.SyncResponse
if err := json.Unmarshal(body, &response); err != nil {
@ -116,12 +151,33 @@ func pushTodoToServer(todo common.Todo) error {
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.GetHttpClient(cfg.Server.Token).Do(req)
token := cfg.Server.Token
resp, err := httpClient.GetHttpClient(token).Do(req)
if err != nil {
return fmt.Errorf("error sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
if common.AskUserBool("Login expired or invalid. Do you wish to login using your stored credentials?") {
err := LoginAndSave(cfg.Server.Url, cfg.Server.Credentials.Username, cfg.Server.Credentials.Password)
if err != nil {
return fmt.Errorf("login failed: %w", err)
}
// Re-send request with new token
req, _ = http.NewRequest("POST", cfg.Server.Url+"/update", bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
newToken := cfg.Server.Token
resp, err = httpClient.GetHttpClient(newToken).Do(req)
if err != nil {
return fmt.Errorf("error sending request after login: %w", err)
}
defer resp.Body.Close()
} else {
return fmt.Errorf("unauthorized: login required to push todo to server")
}
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))

View File

@ -10,11 +10,9 @@ import (
)
func AskUserBool(question string) bool {
switch AskUserString(fmt.Sprintf("%s (Y/N): ", question)) {
case "y", "Y", "yes":
switch strings.ToLower(AskUserString(fmt.Sprintf("%s (Y/N): ", question))) {
case "y", "yes":
return true
case "n", "N", "no":
return false
default:
return false
}
@ -24,7 +22,9 @@ func AskUserString(question string) string {
promptStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true)
fmt.Printf("%s %s", promptStyle.Render(">"), question)
reader := bufio.NewReader(os.Stdin)
input, _, _ := reader.ReadLine()
return strings.TrimSpace(string(input))
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
return strings.TrimSpace(scanner.Text())
}
return ""
}

View File

@ -63,11 +63,28 @@ var (
)
func GetTodoDataStore() (*BoltStore, error) {
return GetCustomDataStore("todo.db")
}
var serverDB *BoltStore
var serverOnce sync.Once
var serverErr error
func GetServerDataStore() (*BoltStore, error) {
serverOnce.Do(func() {
var path string
path, serverErr = getStoragePath()
serverDB, serverErr = NewBoltStore(path + "/server.db")
})
return serverDB, serverErr
}
func GetCustomDataStore(dbName string) (*BoltStore, error) {
once.Do(func() {
var path string
path, err = getStoragePath()
// We assign to the outer 'dataStore' and 'err' variables.
dataStore, err = NewBoltStore(path + "/todo.db")
dataStore, err = NewBoltStore(path + "/" + dbName)
})
if err != nil {
return nil, err

38
go.mod Normal file
View File

@ -0,0 +1,38 @@
module gitea.kleinsense.nl/DariusKlein/kleinTodo/tests
go 1.25.8
require (
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.22 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.etcd.io/bbolt v1.4.3 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace gitea.kleinsense.nl/DariusKlein/kleinTodo/common => ../common
replace gitea.kleinsense.nl/DariusKlein/kleinTodo/server => ../server

57
go.sum Normal file
View File

@ -0,0 +1,57 @@
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39 h1:XxG5TiAP0dvLhRsbwtZDsvCZAiyYJxkOpspAEiLx9po=
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk=
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39 h1:xTwCC3AFY4L5w2Yu8lNYsdsy8yO1eaj2wngdQF4Y3mI=
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39/go.mod h1:wkQ2bz6csXjnKdiqzRgjPt/ZDlABGsFaKdApVsoXJis=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk=
github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,7 +1,8 @@
go 1.25.0
go 1.25.8
use (
./client/todo
./common
./server
./tests
)

View File

@ -13,7 +13,7 @@ func DeleteHandler(w http.ResponseWriter, r *http.Request) {
return
}
store, err := common.GetTodoDataStore()
store, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}

View File

@ -17,7 +17,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Get data store
db, err := common.GetTodoDataStore()
db, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}

View File

@ -16,7 +16,7 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Get data store
db, err := common.GetTodoDataStore()
db, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}

View File

@ -21,7 +21,7 @@ func StoreHandler(w http.ResponseWriter, r *http.Request) {
return
}
store, err := common.GetTodoDataStore()
store, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}

View File

@ -2,6 +2,7 @@ package handler
import (
"encoding/json"
"fmt"
"net/http"
"time"
@ -10,29 +11,35 @@ import (
)
func SyncHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("SyncHandler: starting request processing...")
// 1. Setup: Auth, Decode, Database Connection
user, err := jwt.GetVerifiedUser(r)
if handleError(w, http.StatusUnauthorized, err) {
return
}
fmt.Printf("SyncHandler: authenticated user %s\n", user)
var todoList common.TodoList
if handleError(w, http.StatusBadRequest, json.NewDecoder(r.Body).Decode(&todoList)) {
return
}
fmt.Printf("SyncHandler: received %d todos from client\n", len(todoList.Todos))
store, err := common.GetTodoDataStore()
store, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}
fmt.Println("SyncHandler: connected to datastore")
// 2. Load Server State
serverTodos := store.GetTodoMap(user)
fmt.Printf("SyncHandler: loaded %d server todos\n", len(serverTodos))
response := common.SyncResponse{
SyncedTodos: []common.Todo{},
MisMatchingTodos: []common.MisMatchingTodo{},
}
fmt.Println("SyncHandler: processing client todos...")
// 3. Process Client Updates
for _, clientTodo := range todoList.Todos {
@ -91,11 +98,13 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
}
}
fmt.Println("SyncHandler: building final response...")
// Build Final Response
for _, todo := range serverTodos {
response.SyncedTodos = append(response.SyncedTodos, todo)
}
fmt.Printf("SyncHandler: returning %d synced todos and %d mismatches\n", len(response.SyncedTodos), len(response.MisMatchingTodos))
w.Header().Set("Content-Type", "application/json")
handleError(w, http.StatusInternalServerError, json.NewEncoder(w).Encode(response))
}

View File

@ -20,7 +20,7 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
store, err := common.GetTodoDataStore()
store, err := common.GetServerDataStore()
if handleError(w, http.StatusInternalServerError, err) {
return
}

View File

@ -12,7 +12,7 @@ import (
)
func main() {
db, err := common.GetTodoDataStore()
db, err := common.GetServerDataStore()
if err != nil {
log.Fatal(err)
}

34
tests/go.mod Normal file
View File

@ -0,0 +1,34 @@
module gitea.kleinsense.nl/DariusKlein/kleinTodo/tests
go 1.25.8
require (
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.22 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.etcd.io/bbolt v1.4.3 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

57
tests/go.sum Normal file
View File

@ -0,0 +1,57 @@
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39 h1:XxG5TiAP0dvLhRsbwtZDsvCZAiyYJxkOpspAEiLx9po=
gitea.kleinsense.nl/DariusKlein/kleinTodo/common v0.0.0-20260404111829-b8b839778c39/go.mod h1:owENFzNmtoCmr7ZUjNbkO0i+ugwqKdXCVikfOOcOsWk=
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39 h1:xTwCC3AFY4L5w2Yu8lNYsdsy8yO1eaj2wngdQF4Y3mI=
gitea.kleinsense.nl/DariusKlein/kleinTodo/server v0.0.0-20260404111829-b8b839778c39/go.mod h1:wkQ2bz6csXjnKdiqzRgjPt/ZDlABGsFaKdApVsoXJis=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk=
github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

144
tests/integration_test.go Normal file
View File

@ -0,0 +1,144 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/server/handler"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntegrationSync(t *testing.T) {
// 1. Setup Environment
tempDir, err := os.MkdirTemp("", "todo-test-*")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
// Override storage path for BoltStore by setting HOME or XDG_CONFIG_HOME
// BoltStore uses os.UserConfigDir() which looks at XDG_CONFIG_HOME on Linux
os.Setenv("XDG_CONFIG_HOME", tempDir)
os.Setenv("JWT_SECRET", "test-secret-123")
// 2. Initialize Server
mux := http.NewServeMux()
mux.HandleFunc("POST /register", handler.RegisterHandler)
mux.HandleFunc("POST /login", handler.LoginHandler)
mux.HandleFunc("POST /sync", handler.SyncHandler)
ts := httptest.NewServer(mux)
defer ts.Close()
client := ts.Client()
username := "testuser"
password := "testpass"
// 3. Register User
regPayload, _ := json.Marshal(common.Credentials{Username: username, Password: password})
resp, err := client.Post(ts.URL+"/register", "application/json", bytes.NewBuffer(regPayload))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// 4. Login and Get Token
resp, err = client.Post(ts.URL+"/login", "application/json", bytes.NewBuffer(regPayload))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
token := resp.Header.Get(common.AuthHeader)
require.NotEmpty(t, token)
// 5. Create Local Todos
store, err := common.GetTodoDataStore()
require.NoError(t, err)
defer store.Close()
todo1 := common.Todo{
Id: uuid.New().String(),
Name: "Task 1",
Description: "Desc 1",
Status: common.NotStarted,
Owner: username,
LastModified: time.Now().UTC(),
}
todo2 := common.Todo{
Id: uuid.New().String(),
Name: "Task 2",
Description: "Desc 2",
Status: common.WIP,
Owner: username,
LastModified: time.Now().UTC(),
}
err = todo1.Store(store, username)
require.NoError(t, err)
err = todo2.Store(store, username)
require.NoError(t, err)
// 6. Sync to Server
localTodos := store.GetTodoList(username, true)
syncReq := common.TodoList{Todos: localTodos}
syncPayload, _ := json.Marshal(syncReq)
req, _ := http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(syncPayload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(common.AuthHeader, "Bearer "+token)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var syncResp common.SyncResponse
err = json.NewDecoder(resp.Body).Decode(&syncResp)
require.NoError(t, err)
resp.Body.Close()
// Verify both todos are in sync response
assert.Len(t, syncResp.SyncedTodos, 2)
assert.Empty(t, syncResp.MisMatchingTodos)
// 7. Verify Server State (By syncing again with empty local)
emptySyncReq := common.TodoList{Todos: []common.Todo{}}
emptySyncPayload, _ := json.Marshal(emptySyncReq)
req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(common.AuthHeader, "Bearer "+token)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = json.NewDecoder(resp.Body).Decode(&syncResp)
require.NoError(t, err)
resp.Body.Close()
assert.Len(t, syncResp.SyncedTodos, 2, "Server should have kept the synced todos")
// 8. Test Pulling from Server (New Client)
// Create another temporary directory for a different client
client2TempDir, err := os.MkdirTemp("", "todo-test-client2-*")
require.NoError(t, err)
defer os.RemoveAll(client2TempDir)
// Since common.GetTodoDataStore() uses sync.Once, we can't easily get a new database in the same process
// BUT for this test, we can just use GetCustomDataStore("todo2.db") if it was public,
// however it seems we can just check if the server returns the data when requested.
req, _ = http.NewRequest("POST", ts.URL+"/sync", bytes.NewBuffer(emptySyncPayload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(common.AuthHeader, "Bearer "+token)
resp, err = client.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = json.NewDecoder(resp.Body).Decode(&syncResp)
require.NoError(t, err)
resp.Body.Close()
assert.Len(t, syncResp.SyncedTodos, 2, "Server should return the 2 todos for a sync request")
}