diff --git a/appTodo.go b/appTodo.go index bc7fc67..40b87b3 100644 --- a/appTodo.go +++ b/appTodo.go @@ -22,8 +22,8 @@ func (h FuncHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } var userAuth = web.TokenInfo{ - CookieName: web.COOKIE_NAME, - PrivateKey: web.SECRET_KEY, + CookieName: web.CookieName, + PrivateKey: web.SecretKey, Auth: web.Auth{ Name: "user", IsRequired: true, @@ -36,17 +36,21 @@ func main() { modhandler := FuncHandler{HandlerFunc: web.HandleModifyTodo} todoshandler := FuncHandler{HandlerFunc: web.HandleGetTodos} - cfg := config.GetConfig() + cfg, err := config.GetConfig() + if err != nil { + log.Fatal(err) + } - msql, err := data.OpenDb(cfg) + msql, err := data.OpenDB(&cfg) if err != nil { log.Fatal(err) } - defer data.CloseDb() + err = todos.Init(msql) if err != nil { log.Fatal(err) } + err = users.Init(msql) if err != nil { log.Fatal(err) @@ -62,5 +66,9 @@ func main() { http.HandleFunc("/login", web.HandleLogin) http.HandleFunc("/logout", web.HandleLogout) - http.ListenAndServe(cfg.Port, nil) + err = http.ListenAndServe(cfg.Port, nil) + if err != nil { + log.Fatal(err) + } + defer data.CloseDB() } diff --git a/cli/.cfg/config.json b/cli/.cfg/config.json new file mode 100644 index 0000000..46ca7e0 --- /dev/null +++ b/cli/.cfg/config.json @@ -0,0 +1,4 @@ +{ + "email": "clem@calia.com", + "mdp": "123456" +} \ No newline at end of file diff --git a/cli/README b/cli/README new file mode 100644 index 0000000..61ece57 --- /dev/null +++ b/cli/README @@ -0,0 +1,3 @@ +To use the binary CLI : + + In your terminal from webstack/cli, execute the installer with root privileges : sudo ./install.sh \ No newline at end of file diff --git a/cli/cmd/add.go b/cli/cmd/add.go new file mode 100644 index 0000000..35214f6 --- /dev/null +++ b/cli/cmd/add.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + "webstack/metier/todos" + "webstack/metier/users" +) + +const AddArgsLen = 4 + +func Add(user users.User) { + if len(os.Args) != AddArgsLen { + Help("add") + return + } + + text := os.Args[2] + + priority, err := strconv.Atoi(os.Args[3]) + if err != nil { + fmt.Println(InvArgs) + Help("add") + + return + } + + task, err := todos.NewTask(text) + if err != nil { + fmt.Println(err.Error()) + return + } + + _, err = todos.Add(task, priority, user) + if err != nil { + fmt.Println(err.Error()) + return + } + + Get(user) +} diff --git a/cli/cmd/cmd_test.go b/cli/cmd/cmd_test.go new file mode 100644 index 0000000..b931192 --- /dev/null +++ b/cli/cmd/cmd_test.go @@ -0,0 +1,286 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" + "webstack/metier/todos" + "webstack/metier/users" +) + +type fakeDb struct { + todos []todos.Todo + users []users.User +} + +func (f *fakeDb) AddUserDb(u users.User) error { + for _, user := range f.users { + if users.GetEmail(user) == users.GetEmail(u) { + return fmt.Errorf("email déjà utilisé") + } + } + + f.users = append(f.users, u) + + return nil +} +func (f *fakeDb) GetUser(u users.User) (users.User, error) { + for _, user := range f.users { + fmt.Print(user) + + if users.GetEmail(user) == users.GetEmail(u) { + return user, nil + } + } + + return users.User{}, fmt.Errorf("error") +} + +func (f *fakeDb) AddTodoDb(td todos.Todo, u users.User) error { + f.todos = append(f.todos, td) + return nil +} +func (f *fakeDb) DeleteTodoDb(td todos.Todo) error { + for i, t := range f.todos { + if t.ID == td.ID { + f.todos = append(f.todos[:i], f.todos[i+1:]...) + return nil + } + } + + return nil +} +func (f *fakeDb) ModifyTodoDb(td todos.Todo) error { + for _, t := range f.todos { + if t.ID == td.ID { + t.Task = td.Task + return nil + } + } + + return nil +} +func (f *fakeDb) GetTodosDb(u users.User) (t []todos.Todo, e error) { + t = f.todos + return t, nil +} + +var user users.User + +func setupFakeDb() fakeDb { + db := fakeDb{} + + task1, _ := todos.NewTask("Faire les courses") + task2, _ := todos.NewTask("Sortir le chien") + task3, _ := todos.NewTask("(/$-_~+)=") + task4, _ := todos.NewTask("Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui") + + mdp, _ := users.HashPassword("123456") + todo1 := todos.Todo{ID: 1, Task: task1, Priority: 3} + todo2 := todos.Todo{ID: 2, Task: task2, Priority: 2} + todo3 := todos.Todo{ID: 3, Task: task3, Priority: 2} + todo4 := todos.Todo{ID: 12, Task: task4, Priority: 1} + user, _ = users.NewUser("clem@caramail.fr", mdp) + + db.AddTodoDb(todo1, user) + db.AddTodoDb(todo2, user) + db.AddTodoDb(todo3, user) + db.AddTodoDb(todo4, user) + db.AddUserDb(user) + + return db +} + +func TestGet(t *testing.T) { + db := setupFakeDb() + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + defer func() { + os.Stdout = old + }() + Get(db.users[0]) + + outCh := make(chan string) + go func() { + var buf bytes.Buffer + + _, err = io.Copy(&buf, r) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + outCh <- buf.String() + }() + + err = w.Close() + if err != nil { + t.Fatalf("Expected error to be nil but got : %v", err) + } + + want := todos.GetTask(db.todos[0].Task) + want2 := todos.GetTask(db.todos[1].Task) + actual := <-outCh + + if !strings.Contains(actual, want) || !strings.Contains(actual, want2) { + t.Errorf("expected : %s and %s, but got : %s", want, want2, actual) + } +} + +func TestAdd(t *testing.T) { + db := setupFakeDb() + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + var tests = []struct { + name string + entryText string + entryPrio string + want string + }{ + {"Cas normal", "Sortir le chien", "2", todos.GetTask(db.todos[1].Task)}, + {"Chaîne vide", "", "1", todos.ErrNoText}, + {"Caractères spéciaux autorisés", "(/$-_~+)=", "1", todos.GetTask(db.todos[2].Task)}, + {"Caractères spéciaux non autorisés", "(/$-_]&[~]%)=", "3", todos.ErrSpecialChar}, + {"Plusieurs espaces en entrée", " ", "2", todos.ErrNoText}, + {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", "1", todos.GetTask(db.todos[3].Task)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Args = []string{"go run main.go", "add", tt.entryText, tt.entryPrio} + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + defer func() { + os.Stdout = old + }() + Add(db.users[0]) + outCh := make(chan string) + go func() { + var buf bytes.Buffer + + _, err = io.Copy(&buf, r) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + outCh <- buf.String() + }() + err = w.Close() + if err != nil { + t.Fatalf("Expected error to be nil but got : %v", err) + } + + actual := <-outCh + + if !strings.Contains(actual, tt.want) { + t.Errorf("expected : %s, but got : %s", tt.want, actual) + } + }) + } +} + +func TestDelete(t *testing.T) { + db := setupFakeDb() + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + os.Args = []string{"go run main.go", "delete", "2"} + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + defer func() { + os.Stdout = old + }() + + Delete(db.users[0]) + + outCh := make(chan string) + go func() { + var buf bytes.Buffer + + _, err = io.Copy(&buf, r) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + outCh <- buf.String() + }() + + err = w.Close() + if err != nil { + t.Fatalf("Expected error to be nil but got : %v", err) + } + + dontWant := "Sortir le chien" + actual := <-outCh + + if strings.Contains(actual, dontWant) { + t.Errorf("expected : %s, but got : %s", dontWant, actual) + } +} + +func TestModify(t *testing.T) { + db := setupFakeDb() + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + defer func() { + os.Stdout = old + }() + + os.Args = []string{"go run main.go", "modify", "2", "Todo modifié", "1"} + fmt.Println(os.Args) + Modify(db.users[0]) + + outCh := make(chan string) + go func() { + var buf bytes.Buffer + + _, err = io.Copy(&buf, r) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + outCh <- buf.String() + fmt.Println(outCh) + }() + + err = w.Close() + if err != nil { + t.Fatalf("Expected error to be nil but got : %v", err) + } + + want := "Todo modifié" + actual := <-outCh + + if !strings.Contains(actual, want) { + t.Errorf("expected : %s, but got : %s", want, actual) + } +} diff --git a/cli/cmd/delete.go b/cli/cmd/delete.go new file mode 100644 index 0000000..2089999 --- /dev/null +++ b/cli/cmd/delete.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + "webstack/metier/todos" + "webstack/metier/users" +) + +const ErrSupr = "erreur de suppression de votre tâche" +const DelArgsLen = 3 + +func Delete(u users.User) { + if len(os.Args) != DelArgsLen { + Help("delete") + return + } + + id, err := strconv.Atoi(os.Args[2]) + if err != nil { + fmt.Println(InvArgs) + Help("delete") + + return + } + + _, err = todos.Delete(id) + if err != nil { + fmt.Println(ErrSupr, err) + return + } + + Get(u) +} diff --git a/cli/cmd/get.go b/cli/cmd/get.go new file mode 100644 index 0000000..04b8ff0 --- /dev/null +++ b/cli/cmd/get.go @@ -0,0 +1,94 @@ +package cmd + +import ( + "fmt" + "strconv" + "webstack/metier/todos" + "webstack/metier/users" + + "github.com/jedib0t/go-pretty/v6/table" +) + +const Chill = 1 +const NotChill = 2 +const Emergency = 3 + +const ( + red = "\x1b[31m" + yellow = "\x1b[33m" + green = "\x1b[32m" + reset = "\x1b[0m" +) + +type TodoCli struct { + id string + task string + priority string +} + +func sprintColor(color, text string) string { + return fmt.Sprintf("%s%s%s", color, text, reset) +} + +func NewTodoCli(td todos.Todo) (todocli TodoCli) { + todocli.id = strconv.Itoa(td.ID) + todocli.task = todos.GetTask(td.Task) + + switch td.Priority { + case Chill: + todocli.priority = "chill t'as le temps man" + case NotChill: + todocli.priority = "pas particulièrment pressé" + case Emergency: + todocli.priority = "oula c'est urgent ça" + } + + return todocli +} + +func Todos2TodosCli(list []todos.Todo) (displayedList []TodoCli) { + for _, todo := range list { + displayedList = append(displayedList, NewTodoCli(todo)) + } + + return displayedList +} + +func Get(u users.User) { + columns := []string{"Id", "Todo", "Priorité"} + + list, err := todos.Get(u) + if err != nil { + fmt.Println(err) + return + } + + displayed := Todos2TodosCli(list) + t := table.NewWriter() + t.SetTitle("My Todolist") + t.AppendHeader(table.Row{columns[0], columns[1], columns[2]}) + + for _, display := range displayed { + var colorCode string + + switch display.priority { + case "oula c'est urgent ça": + colorCode = red + case "pas particulièrment pressé": + colorCode = yellow + case "chill t'as le temps man": + colorCode = green + default: + colorCode = reset + } + + row := table.Row{ + sprintColor(colorCode, display.id), + sprintColor(colorCode, display.task), + sprintColor(colorCode, display.priority), + } + t.AppendRow(row) + } + + fmt.Println(t.Render()) +} diff --git a/cli/cmd/help.go b/cli/cmd/help.go new file mode 100644 index 0000000..cdf0b6a --- /dev/null +++ b/cli/cmd/help.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" +) + +func Help(cmd string) { + switch cmd { + case "help": + fmt.Println("My TodoList en CLI !") + fmt.Println("Usage: mytodolist [arguments]") + fmt.Println("\nCommandes disponibles:") + fmt.Println(" get Affiche vos tâches") + fmt.Println(" add Ajouter une nouvelle tâche") + fmt.Println(" delete Supprime une tâche existante") + fmt.Println(" modify Modifie une tâche existante") + fmt.Println(" signin S'inscrire") + fmt.Println(" login Se connecter") + fmt.Println(" logout Se déconnecter") + fmt.Println(" help Affiche ce message") + case "modify": + fmt.Println(`Usage: mytodolist modify "text" `) + fmt.Println("\nid : l'identifiant numérique du todo que vous souhaitez modifier") + fmt.Println("text : une chaîne de caractère décrivant votre todo") + fmt.Println("priority : le niveau de priorité de votre tâche entre 1 et 3, du moins au plus urgent") + case "add": + fmt.Println(`Usage: mytodolist add "text" `) + fmt.Println("\ntext : une chaîne de caractère décrivant votre nouveau todo") + fmt.Println("priority : le niveau de priorité de votre tâche entre 1 et 3, du moins au plus urgent") + case "delete": + fmt.Println("Usage: mytodolist delete ") + fmt.Println("\nid : l'identifiant numérique du todo que vous souhaitez supprimer") + } +} diff --git a/cli/cmd/modify.go b/cli/cmd/modify.go new file mode 100644 index 0000000..bba6235 --- /dev/null +++ b/cli/cmd/modify.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "fmt" + "os" + "strconv" + "webstack/metier/todos" + "webstack/metier/users" +) + +const ModArgsLen = 5 +const InvArgs = "Arguments invalides :" + +func Modify(u users.User) { + if len(os.Args) != ModArgsLen { + Help("modify") + return + } + + text := os.Args[3] + id, err := strconv.Atoi(os.Args[2]) + priority, err2 := strconv.Atoi(os.Args[4]) + + if err != nil || err2 != nil { + fmt.Println(InvArgs) + Help("modify") + + return + } + + task, err := todos.NewTask(text) + if err != nil { + fmt.Println(err.Error()) + return + } + + _, err = todos.Modify(task, id, priority) + if err != nil { + fmt.Println(err.Error()) + return + } + + Get(u) +} diff --git a/cli/install.sh b/cli/install.sh new file mode 100755 index 0000000..9637aff --- /dev/null +++ b/cli/install.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Install mytodolist to /usr/local/bin + +BINARY_PATH="./mytodolist" +INSTALL_DIR="/usr/local/bin" +NEWCONFIG_DIR="/.cfg/mytodolist" +CONFIG_DIR="./.cfg/config.json" + +mkdir -p $NEWCONFIG_DIR + +cp "$BINARY_PATH" "$INSTALL_DIR/" +cp -r "$CONFIG_DIR" "$NEWCONFIG_DIR/" + +# Make it executable +chmod +x "$INSTALL_DIR/mytodolist" + +# Permission to write and read the config +chmod 666 /.cfg/mytodolist/config.json + +echo "mytodolist has been installed to $INSTALL_DIR" diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..043abc2 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "webstack/cli/cmd" + "webstack/cli/user" + "webstack/config" + "webstack/data" + "webstack/metier/todos" + "webstack/metier/users" + + _ "github.com/go-sql-driver/mysql" +) + +const ErrNotACmd = "Commande invalide. Utilisez la commande 'help' pour afficher les commandes disponibles." + +func main() { + cfg, err := config.GetConfig() + if err != nil { + log.Fatal(err) + } + + msql, err := data.OpenDB(&cfg) + if err != nil { + log.Fatal(err) + } + + err = todos.Init(msql) + if err != nil { + log.Fatal(err) + } + + err = users.Init(msql) + if err != nil { + log.Fatal(err) + } + + flag.Parse() + + switch flag.Arg(0) { + case "": + cmd.Help("help") + case "help": + cmd.Help("help") + case "get": + user.Auth(cmd.Get, user.CfgFilePath) + case "add": + user.Auth(cmd.Add, user.CfgFilePath) + case "delete": + user.Auth(cmd.Delete, user.CfgFilePath) + case "modify": + user.Auth(cmd.Modify, user.CfgFilePath) + case "signin": + _, err = user.Signin(user.CfgFilePath) + if err != nil { + fmt.Println(err) + return + } + case "login": + _, err = user.Login(user.CfgFilePath) + if err != nil { + fmt.Println(err) + return + } + case "logout": + err = user.ClearUserConfig(user.CfgFilePath) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Utilisateur déconnecté !") + default: + fmt.Println(ErrNotACmd) + os.Exit(1) + } + defer data.CloseDB() +} diff --git a/cli/main_test.go b/cli/main_test.go new file mode 100644 index 0000000..7681c6b --- /dev/null +++ b/cli/main_test.go @@ -0,0 +1,7 @@ +package main + +import "testing" + +func TestMain(m *testing.M) { + +} diff --git a/cli/mytodolist b/cli/mytodolist new file mode 100755 index 0000000..9e43c78 Binary files /dev/null and b/cli/mytodolist differ diff --git a/cli/user/user.go b/cli/user/user.go new file mode 100644 index 0000000..8b73a43 --- /dev/null +++ b/cli/user/user.go @@ -0,0 +1,226 @@ +package user + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "strings" + "webstack/metier/users" +) + +type UCfg struct { + Email string `json:"email"` + Mdp string `json:"mdp"` +} + +const CfgFilePath = "/.cfg/mytodolist/config.json" +const ErrCfg = "erreur de chargement de la config" +const ErrSaveCfg = "erreur lors de l'enregistrement des informations" +const ErrWrite = "erreur writing updated config" +const ErrRead = "erreur readfile config" +const ErrUnmarsh = "erreur unmarshal data" +const ErrMarsh = "erreur marshal updated data" +const ErrSignin = "erreur pendant l'enregistrement d'un nouvel utilisateur" +const SavedInfo = "Informations sauvegardées." +const NotSignedin = "\nSi ce n'est pas déjà fait pensez à vous inscrire avec la commande signin !" +const NouserCfg = "Aucun utilisateur connecté: Voulez-vous vous connecter (c) ou vous inscrire (i)? " +const InvChoice = "Choix invalide. Choisissez 'c' pour vous connecter ou 'i' pour vous inscrire." +const ErrScan = "erreur de scan" +const FormativeStr = "%v : %v" + +func Auth(f func(u users.User), configFilePath string) func(u users.User) { + configData, err := LoadConfig(configFilePath) + if err != nil { + fmt.Println(ErrCfg, err) + return nil + } + + if configData.Email != "" && configData.Mdp != "" { + u, err := users.NewUser(configData.Email, configData.Mdp) + if err != nil { + fmt.Println(err) + return nil + } + + f(u) + } else { + fmt.Print(NouserCfg) + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + choice := strings.ToLower(scanner.Text()) + switch choice { + case "c": + u, err := Login(configFilePath) + if err != nil { + fmt.Println(err) + } + f(u) + case "i": + u, err := Signin(configFilePath) + if err != nil { + fmt.Println(err) + } + f(u) + default: + fmt.Println(InvChoice) + return nil + } + } + } + + return f +} + +func LoadConfig(configFilePath string) (configUser UCfg, err error) { + if _, err = os.Stat(configFilePath); os.IsNotExist(err) { + configUser = UCfg{} + } else { + fileContent, err := os.ReadFile(configFilePath) + if err != nil { + return configUser, err + } + err = json.Unmarshal(fileContent, &configUser) + if err != nil { + return configUser, err + } + } + + return configUser, nil +} + +func SaveConfig(configUser UCfg, configFilePath string) error { + fileContent, err := json.MarshalIndent(configUser, "", " ") + if err != nil { + return err + } + + err = os.WriteFile(configFilePath, fileContent, 0o644) + if err != nil { + return err + } + + return nil +} + +func ClearUserConfig(configFilePath string) error { + var u UCfg + + data, err := os.ReadFile(configFilePath) + if err != nil { + return err + } + + err = json.Unmarshal(data, &u) + if err != nil { + return fmt.Errorf(FormativeStr, ErrUnmarsh, err) + } + + u.Email = "" + u.Mdp = "" + + updatedData, err := json.MarshalIndent(u, "", " ") + if err != nil { + return fmt.Errorf(FormativeStr, ErrMarsh, err) + } + + err = os.WriteFile(configFilePath, updatedData, 0o644) + if err != nil { + return fmt.Errorf(FormativeStr, ErrWrite, err) + } + + return nil +} + +func NewUserCfg(email, mdp string) (u UCfg) { + u.Email = email + u.Mdp = mdp + + return u +} + +func Login(configFilePath string) (u users.User, err error) { + configData, err := LoadConfig(configFilePath) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrCfg, err) + } + + fmt.Print("Entrez votre email: ") + + _, err = fmt.Scan(&configData.Email) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrScan, err) + } + + fmt.Print("Entrez votre mot de passe: ") + + _, err = fmt.Scan(&configData.Mdp) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrScan, err) + } + + u, err = users.Login(configData.Email, configData.Mdp) + if err != nil { + fmt.Println(NotSignedin) + + err2 := ClearUserConfig(configFilePath) + if err2 != nil { + return u, err2 + } + + return u, err + } + + err = SaveConfig(configData, configFilePath) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrSaveCfg, err) + } + + fmt.Println(SavedInfo) + + return u, nil +} + +func Signin(configFilePath string) (u users.User, err error) { + var confirmmdp string + + configData, err := LoadConfig(configFilePath) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrCfg, err) + } + + fmt.Print("Entrez votre email: ") + + _, err = fmt.Scan(&configData.Email) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrScan, err) + } + + fmt.Print("Choisissez un mot de passe: ") + + _, err = fmt.Scan(&configData.Mdp) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrScan, err) + } + + fmt.Print("Confirmez votre mot de passe: ") + + _, err = fmt.Scan(&confirmmdp) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrScan, err) + } + + u, err = users.Signin(configData.Email, configData.Mdp, confirmmdp) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrSignin, err) + } + + err = SaveConfig(configData, configFilePath) + if err != nil { + return u, fmt.Errorf(FormativeStr, ErrSaveCfg, err) + } + + fmt.Println(SavedInfo) + + return u, nil +} diff --git a/cli/user/user_test.go b/cli/user/user_test.go new file mode 100644 index 0000000..08df841 --- /dev/null +++ b/cli/user/user_test.go @@ -0,0 +1,176 @@ +package user + +import ( + "fmt" + "testing" + "webstack/metier/todos" + "webstack/metier/users" +) + +const TestCfg = "../.cfg/config.json" + +type fakeDB struct { + todos []todos.Todo + users []users.User +} + +func (f *fakeDB) AddUserDb(u users.User) error { + for _, user := range f.users { + if users.GetEmail(user) == users.GetEmail(u) { + return fmt.Errorf("email déjà utilisé") + } + } + + f.users = append(f.users, u) + + return nil +} +func (f *fakeDB) GetUser(u users.User) (users.User, error) { + for _, user := range f.users { + fmt.Print(user) + + if users.GetEmail(user) == users.GetEmail(u) { + return user, nil + } + } + + return users.User{}, fmt.Errorf("error") +} + +func (f *fakeDB) AddTodoDb(td todos.Todo, u users.User) error { + f.todos = append(f.todos, td) + return nil +} +func (f *fakeDB) DeleteTodoDb(td todos.Todo) error { + for i, t := range f.todos { + if t.ID == td.ID { + f.todos = append(f.todos[:i], f.todos[i+1:]...) + return nil + } + } + + return nil +} +func (f *fakeDB) ModifyTodoDb(td todos.Todo) error { + for _, t := range f.todos { + if t.ID == td.ID { + t.Task = td.Task + return nil + } + } + + return nil +} +func (f *fakeDB) GetTodosDb(u users.User) (t []todos.Todo, e error) { + t = f.todos + return t, nil +} + +var user users.User + +func setupFakeDB() fakeDB { + db := fakeDB{} + + task1, _ := todos.NewTask("Faire les courses") + task2, _ := todos.NewTask("Sortir le chien") + task3, _ := todos.NewTask("(/$-_~+)=") + task4, _ := todos.NewTask("Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui") + + mdp, _ := users.HashPassword("123456") + todo1 := todos.Todo{ID: 1, Task: task1, Priority: 3} + todo2 := todos.Todo{ID: 2, Task: task2, Priority: 2} + todo3 := todos.Todo{ID: 3, Task: task3, Priority: 2} + todo4 := todos.Todo{ID: 12, Task: task4, Priority: 1} + user, _ = users.NewUser("clem@caramail.fr", mdp) + + db.AddTodoDb(todo1, user) + db.AddTodoDb(todo2, user) + db.AddTodoDb(todo3, user) + db.AddTodoDb(todo4, user) + db.AddUserDb(user) + + return db +} + +// Sans doute plus interessant de tester SaveConfig, LoadConfig, ClearUserConfig et NewUserCfg que Login et Signin +func TestUserConfig(t *testing.T) { + var tests = []struct { + name, entryEmail, entryPassword string + }{ + {"Cas normal", "mail2018@mail.com", "29mai1995"}, + {"Mots de passes différents", "mail2019@mail.com", "29mai1995"}, + {"Email vide", "", "12azerty"}, + {"Email invalide", "mail2018mailcom", "29mai1995"}, + {"Mot de passe trop court", "mail@mail.com", "azey"}, + {"Email déjà utilisé", "mail20@mail.com", "2mai1995"}, + {"Mot de passe vide", "clem@caramail.com", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := NewUserCfg(tt.entryEmail, tt.entryPassword) + + err := SaveConfig(u, TestCfg) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + + configData, err := LoadConfig(TestCfg) + if err != nil { + t.Errorf("expected error to be nil but got : %v", err) + } + if u != configData { + t.Errorf("expected : %v, but got : %v", u, configData) + } + }) + } +} + +func TestClearUserConfig(t *testing.T) { + db := setupFakeDB() + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + var tests = []struct { + name, entryEmail, entryPassword string + }{ + {"Cas normal", "mail2018@mail.com", "29mai1995"}, + {"Mots de passes différents", "mail2019@mail.com", "29mai1995"}, + {"Email vide", "", "12azerty"}, + {"Email invalide", "mail2018mailcom", "29mai1995"}, + {"Email déjà utilisé", "mail20@mail.com", "2mai1995"}, + {"Mot de passe vide", "clem@caramail.com", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := NewUserCfg(tt.entryEmail, tt.entryPassword) + + err := SaveConfig(u, TestCfg) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + + configData, err := LoadConfig(TestCfg) + if err != nil { + t.Errorf("expected error to be nil but got : %v", err) + } else if configData.Email == "" && configData.Mdp == "" { + t.Errorf("expected : %v, but got nothing", configData) + } + + err = ClearUserConfig(TestCfg) + if err != nil { + t.Errorf("Expected error to be nil but got : %v", err) + } + + emptyConfigData, err := LoadConfig(TestCfg) + if (emptyConfigData.Email != "" && emptyConfigData.Mdp != "") || err != nil { + t.Errorf("expected : %v to be empty but it's not", emptyConfigData) + } + }) + } +} diff --git a/config/config.go b/config/config.go index 699ff0e..8aa6646 100644 --- a/config/config.go +++ b/config/config.go @@ -1,9 +1,12 @@ package config import ( + "fmt" "os" ) +const ErrInvConfig = "configuration invalide" + type Config struct { StaticDir string Port string @@ -22,7 +25,7 @@ var servConfig = Config{ Dbpsw: "adminPassword", } -func GetConfig() Config { +func GetConfig() (Config, error) { // détermine si base de donnée locale ou dans container dbs := os.Getenv("DBS") if dbs == "tcp" { @@ -34,5 +37,10 @@ func GetConfig() Config { if dir != "" { servConfig.StaticDir = dir } - return servConfig + + if servConfig.Port == "" || servConfig.Db == "" { + return Config{}, fmt.Errorf(ErrInvConfig) + } + + return servConfig, nil } diff --git a/config/config_test.go b/config/config_test.go index 85f6f90..9dd6ff5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,11 +4,13 @@ import "testing" func TestGetConfig(t *testing.T) { var dbsrc string + var tests = []struct { name, dbs, dir string + wantErr error }{ - {"Config par défaut (dev)", "", "./"}, - {"Config prod", "tcp", "./ihm"}, + {"Config par défaut (dev)", "", "./", nil}, + {"Config prod", "tcp", "./ihm", nil}, } for _, tt := range tests { @@ -20,9 +22,9 @@ func TestGetConfig(t *testing.T) { } else { dbsrc = "" } - got := GetConfig() + got, gotErr := GetConfig() want := Config{StaticDir: tt.dir, Port: ":5050", Db: "todos", Dbsrc: dbsrc, Dbusr: "adminUser", Dbpsw: "adminPassword"} - if got != want { + if got != want && gotErr != tt.wantErr { t.Errorf("got %q, wanted %q", got, want) } }) diff --git a/data/data.go b/data/data.go index 0481044..1d3b220 100644 --- a/data/data.go +++ b/data/data.go @@ -14,37 +14,41 @@ type MysqlServer struct { var db *sql.DB -const ERR_MAILUSED = "email déjà utilisé" -const ERR_NOUSER = "utilisateur introuvable" - -func OpenDb(cfg config.Config) (m MysqlServer, err error) { +const ErrMailUsed = "email déjà utilisé" +const ErrNoUser = "utilisateur introuvable" +func OpenDB(cfg *config.Config) (m MysqlServer, err error) { urldb := fmt.Sprintf("%s:%s@%s/%s", cfg.Dbusr, cfg.Dbpsw, cfg.Dbsrc, cfg.Db) + db, err = sql.Open("mysql", urldb) if err != nil { return m, fmt.Errorf("sql Open() : %v", err) } + return m, nil } -func CloseDb() error { +func CloseDB() error { return db.Close() } -func (m MysqlServer) AddUserDb(user users.User) error { +func (m MysqlServer) AddUserDb(u users.User) error { var count int - err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", users.GetEmail(user)).Scan(&count) + + err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", users.GetEmail(u)).Scan(&count) if err != nil { return fmt.Errorf("AddUser error : %v", err) } if count > 0 { - return fmt.Errorf(ERR_MAILUSED) + return fmt.Errorf(ErrMailUsed) } - _, err = db.Exec("INSERT INTO users (email, password) VALUES (?,?)", users.GetEmail(user), users.GetMdp(user)) + + _, err = db.Exec("INSERT INTO users (email, password) VALUES (?,?)", users.GetEmail(u), users.GetMdp(u)) if err != nil { return fmt.Errorf("AddUser error : %v", err) } + return nil } @@ -54,37 +58,46 @@ func (m MysqlServer) GetUser(u users.User) (users.User, error) { err := db.QueryRow("SELECT password FROM users WHERE email = ?", users.GetEmail(u)).Scan(&storedPassword) if err != nil { if err == sql.ErrNoRows { - return users.User{}, fmt.Errorf(ERR_NOUSER) + return users.User{}, fmt.Errorf(ErrNoUser) } + return users.User{}, fmt.Errorf("erreur de connexion à la base de donnée : %v", err) } + u = users.SetMdp(storedPassword) + return u, nil } func (m MysqlServer) GetTodosDb(u users.User) ([]todos.Todo, error) { var list []todos.Todo + var text string + rows, err := db.Query("SELECT todoid, text, priority FROM todos JOIN users ON todos.userid = users.userid WHERE users.email = (?)", users.GetEmail(u)) if err != nil { return nil, fmt.Errorf("GetTodos error : %v", err) } defer rows.Close() + for rows.Next() { todo := todos.Todo{} - var text string - if err := rows.Scan(&todo.Id, &text, &todo.Priority); err != nil { + if err2 := rows.Scan(&todo.ID, &text, &todo.Priority); err2 != nil { return nil, fmt.Errorf("GetTodos error : %v", err) } + todo.Task, err = todos.NewTask(text) if err != nil { return nil, err } + list = append(list, todo) } + if err := rows.Err(); err != nil { return nil, fmt.Errorf("GetTodos error : %v", err) } + return list, nil } @@ -93,21 +106,24 @@ func (m MysqlServer) AddTodoDb(td todos.Todo, u users.User) error { if err != nil { return fmt.Errorf("addTodo error : %v", err) } + return nil } func (m MysqlServer) DeleteTodoDb(td todos.Todo) error { - _, err := db.Exec("DELETE FROM todos WHERE todoid = (?)", td.Id) + _, err := db.Exec("DELETE FROM todos WHERE todoid = (?)", td.ID) if err != nil { return fmt.Errorf("deleteTodo error : %v", err) } + return nil } func (m MysqlServer) ModifyTodoDb(td todos.Todo) error { - _, err := db.Exec("UPDATE todos SET text = (?), priority = (?) WHERE todoid = (?)", todos.GetTask(td.Task), td.Priority, td.Id) + _, err := db.Exec("UPDATE todos SET text = (?), priority = (?) WHERE todoid = (?)", todos.GetTask(td.Task), td.Priority, td.ID) if err != nil { return fmt.Errorf("modifyTodo error : %v", err) } + return nil } diff --git a/data/data_test.go b/data/data_test.go index 3994fdc..6d8b9b8 100644 --- a/data/data_test.go +++ b/data/data_test.go @@ -7,7 +7,12 @@ import ( // A faire en test d'integration, ne valide pas la logique des fonctions mais l'infrastructure de l'application func TestOpenDb(t *testing.T) { - got, _ := OpenDb(config.GetConfig()) + cfg, err := config.GetConfig() + if err != nil { + t.Fatal(err) + } + + got, _ := OpenDB(&cfg) want := MysqlServer{} if got != want { diff --git a/go.mod b/go.mod index 52e3ae3..65f5162 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,14 @@ require github.com/go-sql-driver/mysql v1.8.0 require golang.org/x/crypto v0.21.0 +require ( + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) + require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/jedib0t/go-pretty/v6 v6.5.8 ) diff --git a/ihm/app.svelte b/ihm/app.svelte index fcacf26..3b9fd0a 100644 --- a/ihm/app.svelte +++ b/ihm/app.svelte @@ -16,17 +16,21 @@ export function answerResponse(text,statusCode) { try { - if (statusCode == 403) { - alert(`${text}reconnectez vous`); - redirectTo("index.html"); - } else if (statusCode == 500) { - alert(`${text}réessayez`); - } else if (statusCode == 401) { - alert(`${text}échec d'authentification`); - }else { - alert(`${text}`); - console.log("statut d'erreur inattendu :", statusCode); - } + const statusMessages = { + 403: `${text}reconnectez vous`, + 500: `${text}réessayez`, + 401: `${text}échec d'authentification`, + 400: `${text}`, + }; + if (statusMessages[statusCode]) { + alert(statusMessages[statusCode]); + if (statusCode === 403) { + redirectTo("index.html"); + } + } else { + alert(`${text}`); + console.log("statut d'erreur inattendu :", statusCode); + } } catch (error) { console.error("erreur d'analyse de la réponse du serveur :", error); } diff --git a/ihm/style/style.css b/ihm/style/style.css index 6f7ed01..e48f75c 100644 --- a/ihm/style/style.css +++ b/ihm/style/style.css @@ -10,6 +10,7 @@ form{ margin: 2%; } label{ + margin: 1%; font-size: medium; } p{ @@ -49,8 +50,14 @@ li { margin: 3%; } input { - flex: 0.75; padding: 0.5em; margin: 1%; - border: none; + border: solid 2px; + border-radius: 0.5em; + border-color: rgba(252, 252, 252, 0); +} +input:hover { + cursor: text; + border: solid 2px; + border-color: rgba(0, 0, 0, 0.29); } \ No newline at end of file diff --git a/metier/todos/todos.go b/metier/todos/todos.go index 3520a4e..92f25cd 100644 --- a/metier/todos/todos.go +++ b/metier/todos/todos.go @@ -12,37 +12,43 @@ type Task struct { text string } type Todo struct { - Id int `json:"id"` + ID int `json:"id"` Task Task `json:"task"` Priority int `json:"priority"` } var store DatabaseTodo -const ERR_SPECIAL_CHAR = "caractères spéciaux non autorisés : {}[]|" -const ERR_NO_TEXT = "veuillez renseigner du texte" -const ERR_NO_ID = "todo introuvable, réessayez ultérieurement" -const ERR_GETDATA = "erreur lors de la récupération des données" -const ERR_DBNIL = "error db nil" +const ErrSpecialChar = "caractères spéciaux non autorisés : {}[]|" +const ErrNoText = "veuillez renseigner du texte" +const ErrNoID = "todo introuvable, réessayez ultérieurement" +const ErrGetData = "erreur lors de la récupération des données" +const ErrDBNil = "error db nil" +const ErrPriority = "priorité de tâche invalide" func Init(db DatabaseTodo) error { if db == nil { - return fmt.Errorf(ERR_DBNIL) + return fmt.Errorf(ErrDBNil) } + store = db + return nil } func NewTask(text string) (t Task, err error) { if containsSpecialCharacters(text) { - err = fmt.Errorf(ERR_SPECIAL_CHAR) + err = fmt.Errorf(ErrSpecialChar) return t, err } + if containsOnlySpaces(text) { - err = fmt.Errorf(ERR_NO_TEXT) + err = fmt.Errorf(ErrNoText) return t, err } + t.text = text + return t, nil } @@ -64,44 +70,57 @@ func sortByPriorityDesc(todos []Todo) []Todo { sort.Slice(todos, func(i, j int) bool { return todos[i].Priority > todos[j].Priority }) + return todos } func Get(u users.User) ([]Todo, error) { list, err := store.GetTodosDb(u) if err != nil { - return nil, fmt.Errorf("%v : %v", ERR_GETDATA, err) + return nil, fmt.Errorf("%v : %v", ErrGetData, err) } + listDesc := sortByPriorityDesc(list) + return listDesc, nil } func Add(text Task, priority int, user users.User) (td Todo, err error) { - td.Task = text + if priority < 1 || priority > 3 { + return td, fmt.Errorf("%v: %v", ErrPriority, err) + } + td.Priority = priority + td.Task = text + err = store.AddTodoDb(td, user) if err != nil { return td, err } + return td, nil } func Delete(id int) (td Todo, err error) { - td.Id = id + td.ID = id err = store.DeleteTodoDb(td) + if err != nil { return td, err } + return td, nil } -func Modify(text Task, id int, priority int) (td Todo, err error) { - td.Id = id +func Modify(text Task, id, priority int) (td Todo, err error) { + td.ID = id td.Task = text td.Priority = priority err = store.ModifyTodoDb(td) + if err != nil { return td, err } + return td, nil } diff --git a/metier/todos/todos_test.go b/metier/todos/todos_test.go index c23146d..71332e5 100644 --- a/metier/todos/todos_test.go +++ b/metier/todos/todos_test.go @@ -23,21 +23,22 @@ func (f *fakeDb) AddTodoDb(td Todo, u users.User) error { } func (f *fakeDb) DeleteTodoDb(td Todo) error { for i, t := range f.todos { - if t.Id == td.Id { + if t.ID == td.ID { f.todos = append(f.todos[:i], f.todos[i+1:]...) return nil } } - return nil + return nil } func (f *fakeDb) ModifyTodoDb(td Todo) error { for _, t := range f.todos { - if t.Id == td.Id { + if t.ID == td.ID { t.Task = td.Task return nil } } + return nil } func (f *fakeDb) GetTodosDb(u users.User) (t []Todo, e error) { @@ -55,10 +56,10 @@ func setupFakeDb() fakeDb { task3, _ := NewTask("(/$-_~+)=") task4, _ := NewTask("Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui") - todo1 := Todo{Id: 1, Task: task1, Priority: 3} - todo2 := Todo{Id: 2, Task: task2, Priority: 2} - todo3 := Todo{Id: 3, Task: task3, Priority: 2} - todo4 := Todo{Id: 12, Task: task4, Priority: 1} + todo1 := Todo{ID: 1, Task: task1, Priority: 3} + todo2 := Todo{ID: 2, Task: task2, Priority: 2} + todo3 := Todo{ID: 3, Task: task3, Priority: 2} + todo4 := Todo{ID: 12, Task: task4, Priority: 1} db.AddTodoDb(todo1, user) db.AddTodoDb(todo2, user) @@ -70,7 +71,11 @@ func setupFakeDb() fakeDb { func TestGetTodos(t *testing.T) { db := setupFakeDb() - Init(&db) + + err := Init(&db) + if err != nil { + t.Fatal(err) + } want := db.todos got, err := Get(user) @@ -78,10 +83,12 @@ func TestGetTodos(t *testing.T) { if err != nil { t.Errorf("Erreur lors de la récupération des données : %v", err) } + if len(got) != len(want) { t.Errorf("Expected %d items, but got %d items", len(want), len(got)) return } + for i := range want { if got[i] != want[i] { t.Errorf("Expecte item %d to be '%v', but got '%v'", i, want[i], got[i]) @@ -91,7 +98,11 @@ func TestGetTodos(t *testing.T) { func TestAddTodo(t *testing.T) { db := setupFakeDb() - Init(&db) + + err := Init(&db) + if err != nil { + t.Fatal(err) + } var tests = []struct { name string @@ -100,10 +111,10 @@ func TestAddTodo(t *testing.T) { want string }{ {"Cas normal", "Sortir le chien", 2, db.todos[1].Task.text}, - {"Chaîne vide", "", 1, ERR_NO_TEXT}, + {"Chaîne vide", "", 1, ErrNoText}, {"Caractères spéciaux autorisés", "(/$-_~+)=", 1, db.todos[2].Task.text}, - {"Caractères spéciaux non autorisés", "(/$-_]&[~]%)=", 3, ERR_SPECIAL_CHAR}, - {"Plusieurs espaces en entrée", " ", 2, ERR_NO_TEXT}, + {"Caractères spéciaux non autorisés", "(/$-_]&[~]%)=", 3, ErrSpecialChar}, + {"Plusieurs espaces en entrée", " ", 2, ErrNoText}, {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", 1, db.todos[3].Task.text}, } @@ -123,11 +134,15 @@ func TestAddTodo(t *testing.T) { func TestDeleteTodo(t *testing.T) { db := fakeDb{} - Init(&db) + + err := Init(&db) + if err != nil { + t.Fatal(err) + } var tests = []struct { name string - entryId int + entryID int want string }{ {"Cas normal", 3, "3"}, @@ -137,12 +152,12 @@ func TestDeleteTodo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Delete(tt.entryId) - gotJson, err2 := json.Marshal(got) + got, err := Delete(tt.entryID) + gotJSON, err2 := json.Marshal(got) if err2 != nil { panic(err2) } - if (!strings.Contains(string(gotJson), tt.want)) && !strings.Contains(err.Error(), tt.want) { + if (!strings.Contains(string(gotJSON), tt.want)) && !strings.Contains(err.Error(), tt.want) { t.Errorf("expected response to contain '%s', but got '%s'", tt.want, err.Error()) } }) @@ -151,26 +166,30 @@ func TestDeleteTodo(t *testing.T) { func TestModifyTodo(t *testing.T) { db := setupFakeDb() - Init(&db) + + err := Init(&db) + if err != nil { + t.Fatal(err) + } var tests = []struct { name string entryTxt string - entryId int + entryID int entryPrio int want string }{ {"Cas normal", "Sortir le chien", 3, 2, db.todos[1].Task.text}, - {"Chaîne vide", "", 123, 1, ERR_NO_TEXT}, + {"Chaîne vide", "", 123, 1, ErrNoText}, {"Caractères spéciaux autorisés", "(/$-_~+)=", 3, 2, "(/$-_~+)="}, - {"Caractères spéciaux non autorisés", "(/${}-_~+)=", 13, 1, ERR_SPECIAL_CHAR}, + {"Caractères spéciaux non autorisés", "(/${}-_~+)=", 13, 1, ErrSpecialChar}, {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", 12, 1, "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { task, err := NewTask(tt.entryTxt) - got, _ := Modify(task, tt.entryId, tt.entryPrio) + got, _ := Modify(task, tt.entryID, tt.entryPrio) if err != nil && err.Error() != tt.want { t.Errorf("expected response to contain '%s', but got '%s'", tt.want, err) } diff --git a/metier/users/users.go b/metier/users/users.go index 302a36a..87f4129 100644 --- a/metier/users/users.go +++ b/metier/users/users.go @@ -14,21 +14,24 @@ type User struct { var store DatabaseUser -const ERR_LOGIN = "échec du login" -const ERR_NOMAIL = "l'email ne peut pas être vide" -const ERR_INVMAIL = "email invalide" -const ERR_NOMDP = "le mot de passe ne peut pas être vide" -const ERR_BADMDP = "mot de passe incorrect" -const ERR_DIFMDP = "mots de passe différents" -const ERR_SHORTMDP = "mot de passe trop court (6 caractères minimum)" -const ERR_HASHMDP = "erreur d'encodage du mot de passe" -const ERR_DBNIL = "error db nil" +const ErrLogin = "échec du login" +const ErrNoMail = "l'email ne peut pas être vide" +const ErrInvMail = "email invalide" +const ErrNoMdp = "le mot de passe ne peut pas être vide" +const ErrBadMdp = "mot de passe incorrect" +const ErrDifMdp = "mots de passe différents" +const ErrShortMdp = "mot de passe trop court (6 caractères minimum)" +const ErrHashMdp = "erreur d'encodage du mot de passe" +const ErrDBNil = "erreur db nil" +const MinPasswordLen = 6 func Init(db DatabaseUser) error { if db == nil { - return fmt.Errorf(ERR_DBNIL) + return fmt.Errorf(ErrDBNil) } + store = db + return nil } @@ -50,54 +53,65 @@ func SetEmail(mail string) (u User) { return u } -func NewUser(email string, mdp string) (u User, err error) { +func NewUser(email, mdp string) (u User, err error) { if email == "" { - return u, fmt.Errorf("%v", ERR_NOMAIL) + return u, fmt.Errorf("%v", ErrNoMail) } + if mdp == "" { - return u, fmt.Errorf("%v", ERR_NOMDP) - } else if len(mdp) < 6 { - return u, fmt.Errorf("%v", ERR_SHORTMDP) + return u, fmt.Errorf("%v", ErrNoMdp) + } else if len(mdp) < MinPasswordLen { + return u, fmt.Errorf("%v", ErrShortMdp) } + if !strings.Contains(email, "@") { - return u, fmt.Errorf("%v", ERR_INVMAIL) + return u, fmt.Errorf("%v", ErrInvMail) } + u.email = email u.mdp = mdp + return u, nil } -func Signin(email string, mdp string, confirmmdp string) (u User, err error) { +func Signin(email, mdp, confirmmdp string) (u User, err error) { if mdp != confirmmdp { - return u, fmt.Errorf("%v", ERR_DIFMDP) + return u, fmt.Errorf("%v", ErrDifMdp) } + u, err = NewUser(email, mdp) if err != nil { return u, err } + u.mdp, err = HashPassword(mdp) if err != nil { - return u, fmt.Errorf("%v : %v", ERR_HASHMDP, err) + return u, fmt.Errorf("%v : %v", ErrHashMdp, err) } + err = store.AddUserDb(u) if err != nil { return u, err } + return u, nil } -func Login(email string, mdp string) (u User, err error) { +func Login(email, mdp string) (u User, err error) { u, err = NewUser(email, mdp) if err != nil { return u, err } + user, err := store.GetUser(u) if err != nil { - return u, fmt.Errorf("%v : %v", ERR_LOGIN, err.Error()) + return u, fmt.Errorf("%v : %v", ErrLogin, err.Error()) } + if !checkPasswordHash(u.mdp, user.mdp) { - return u, fmt.Errorf(ERR_BADMDP) + return u, fmt.Errorf(ErrBadMdp) } + return u, nil } diff --git a/metier/users/users_test.go b/metier/users/users_test.go index 2818e26..6298d31 100644 --- a/metier/users/users_test.go +++ b/metier/users/users_test.go @@ -51,10 +51,10 @@ func TestLogin(t *testing.T) { name, entryEmail, entryPassword, want string }{ {"Cas normal", "mail20@mail.com", "25mai1995", "mail20@mail.com"}, - {"Email vide", "", "12azerty", ERR_NOMAIL}, - {"Mot de passe incorrect", "mail20@mail.com", "azerty", ERR_BADMDP}, - {"Email invalide", "ma@mail.com", "25mai1995", ERR_LOGIN}, - {"Mot de passe vide", "clement@caramail.com", "", ERR_NOMDP}, + {"Email vide", "", "12azerty", ErrNoMail}, + {"Mot de passe incorrect", "mail20@mail.com", "azerty", ErrBadMdp}, + {"Email invalide", "ma@mail.com", "25mai1995", ErrLogin}, + {"Mot de passe vide", "clement@caramail.com", "", ErrNoMdp}, } for _, tt := range tests { @@ -75,12 +75,12 @@ func TestSignin(t *testing.T) { name, entryEmail, entryPassword, entryConfirm, want string }{ {"Cas normal", "mail2018@mail.com", "29mai1995", "29mai1995", "mail2018@mail.com"}, - {"Mots de passes différents", "mail2019@mail.com", "29mai1995", "2mai1995", ERR_DIFMDP}, - {"Email vide", "", "12azerty", "12azerty", ERR_NOMAIL}, - {"Email invalide", "mail2018mailcom", "29mai1995", "29mai1995", ERR_INVMAIL}, - {"Mot de passe trop court", "mail@mail.com", "azey", "azey", ERR_SHORTMDP}, + {"Mots de passes différents", "mail2019@mail.com", "29mai1995", "2mai1995", ErrDifMdp}, + {"Email vide", "", "12azerty", "12azerty", ErrNoMail}, + {"Email invalide", "mail2018mailcom", "29mai1995", "29mai1995", ErrInvMail}, + {"Mot de passe trop court", "mail@mail.com", "azey", "azey", ErrShortMdp}, {"Email déjà utilisé", "mail20@mail.com", "2mai1995", "2mai1995", "email déjà utilisé"}, - {"Mot de passe vide", "clem@caramail.com", "", "", ERR_NOMDP}, + {"Mot de passe vide", "clem@caramail.com", "", "", ErrNoMdp}, } for _, tt := range tests { diff --git a/web/web.go b/web/web.go index 3d7e492..7094c42 100644 --- a/web/web.go +++ b/web/web.go @@ -9,23 +9,25 @@ import ( "webstack/metier/users" ) -const ERR_AJOUT = "erreur d'ajout de votre tâche" -const ERR_SUPR = "erreur de suppression de votre tâche" -const ERR_MODIF = "erreur de modification de votre tâche" -const ERR_GETDATA = "erreur lors de la récupération des données" -const ERR_ENCOD = "erreur d'encodage json" -const ERR_CONV = "erreur de conversion" +const ErrAjout = "erreur d'ajout de votre tâche" +const ErrSupr = "erreur de suppression de votre tâche" +const ErrModif = "erreur de modification de votre tâche" +const ErrGetData = "erreur lors de la récupération des données" +const ErrEncod = "erreur d'encodage json" +const ErrConv = "erreur de conversion" +const FormativeStr = "%v : %v" type TodoView struct { - Id int `json:"id"` + ID int `json:"id"` Task string `json:"task"` Priority int `json:"priority"` } func NewTodoView(td todos.Todo) (tdv TodoView) { - tdv.Id = td.Id + tdv.ID = td.ID tdv.Task = todos.GetTask(td.Task) tdv.Priority = td.Priority + return tdv } @@ -33,36 +35,43 @@ func Todos2TodosView(list []todos.Todo) (displayedList []TodoView) { for _, todo := range list { displayedList = append(displayedList, NewTodoView(todo)) } + return displayedList } func encodejson(w http.ResponseWriter, a any) (any, error) { w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(a) if err != nil { - return nil, fmt.Errorf("%v : %v", ERR_ENCOD, err) + return nil, fmt.Errorf("%v : %v", ErrEncod, err) } + return a, nil } func HandleAddTodo(w http.ResponseWriter, r *http.Request) { var user users.User + text := r.FormValue("task") priorityStr := r.FormValue("priority") priority, err := strconv.Atoi(priorityStr) if err != nil { - err = fmt.Errorf("%v : %v", ERR_CONV, err) + err = fmt.Errorf("%v : %v", ErrConv, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } - tokenStr, err := getTokenString(r, COOKIE_NAME) + + tokenStr, err := getTokenString(r, CookieName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } user = users.SetEmail(getUserEmail(tokenStr)) + task, err := todos.NewTask(text) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -71,17 +80,19 @@ func HandleAddTodo(w http.ResponseWriter, r *http.Request) { todo, err := todos.Add(task, priority, user) if err != nil { - err = fmt.Errorf("%v : %v", ERR_AJOUT, err) + err = fmt.Errorf("%v : %v", ErrAjout, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } + todoview := NewTodoView(todo) + _, err = encodejson(w, todoview) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - } func HandleDeleteTodo(w http.ResponseWriter, r *http.Request) { @@ -89,18 +100,22 @@ func HandleDeleteTodo(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(idStr) if err != nil || id == 0 { - err = fmt.Errorf("%v : %v", ERR_CONV, err) + err = fmt.Errorf("%v : %v", ErrConv, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } todo, err := todos.Delete(id) if err != nil { - err = fmt.Errorf("%v : %v", ERR_SUPR, err) + err = fmt.Errorf("%v : %v", ErrSupr, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } + todoview := NewTodoView(todo) + _, err = encodejson(w, todoview) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -115,14 +130,17 @@ func HandleModifyTodo(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(idStr) if err != nil { - err = fmt.Errorf("%v : %v", ERR_CONV, err) + err = fmt.Errorf(FormativeStr, ErrConv, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } + priority, err := strconv.Atoi(priorityStr) if err != nil { - err = fmt.Errorf("%v : %v", ERR_CONV, err) + err = fmt.Errorf(FormativeStr, ErrConv, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } @@ -131,13 +149,17 @@ func HandleModifyTodo(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } + todo, err := todos.Modify(task, id, priority) if err != nil { - err = fmt.Errorf("%v : %v", ERR_MODIF, err) + err = fmt.Errorf(FormativeStr, ErrModif, err) http.Error(w, err.Error(), http.StatusInternalServerError) + return } + todoview := NewTodoView(todo) + _, err = encodejson(w, todoview) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -147,7 +169,8 @@ func HandleModifyTodo(w http.ResponseWriter, r *http.Request) { func HandleGetTodos(w http.ResponseWriter, r *http.Request) { var user users.User - tokenStr, err := getTokenString(r, COOKIE_NAME) + + tokenStr, err := getTokenString(r, CookieName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -156,10 +179,12 @@ func HandleGetTodos(w http.ResponseWriter, r *http.Request) { user = users.SetEmail(getUserEmail(tokenStr)) list, err := todos.Get(user) displayedList := Todos2TodosView(list) + if err != nil { - http.Error(w, ERR_GETDATA, http.StatusInternalServerError) + http.Error(w, ErrGetData, http.StatusInternalServerError) return } + _, err = encodejson(w, displayedList) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/web/web_test.go b/web/web_test.go index b5a9d83..5523c76 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -16,14 +16,14 @@ import ( func TestEncodejson(t *testing.T) { var tests = []struct { - Id int + ID int Text string }{ - {Id: 10, Text: "Blabla"}, - {Id: 123, Text: "(/$-_~+)="}, - {Id: 516, Text: ""}, - {Id: 0, Text: "(/$-_]&[~]%)="}, - {Id: 56, Text: "text"}, + {ID: 10, Text: "Blabla"}, + {ID: 123, Text: "(/$-_~+)="}, + {ID: 516, Text: ""}, + {ID: 0, Text: "(/$-_]&[~]%)="}, + {ID: 56, Text: "text"}, } for i, tt := range tests { @@ -52,16 +52,18 @@ func (f *fakeDb) AddUserDb(u users.User) error { return fmt.Errorf("email déjà utilisé") } } + f.users = append(f.users, u) + return nil } func (f *fakeDb) GetUser(u users.User) (users.User, error) { for _, user := range f.users { - fmt.Print(user) if users.GetEmail(user) == users.GetEmail(u) { return user, nil } } + return users.User{}, fmt.Errorf("error") } @@ -71,21 +73,22 @@ func (f *fakeDb) AddTodoDb(td todos.Todo, u users.User) error { } func (f *fakeDb) DeleteTodoDb(td todos.Todo) error { for i, t := range f.todos { - if t.Id == td.Id { + if t.ID == td.ID { f.todos = append(f.todos[:i], f.todos[i+1:]...) return nil } } - return nil + return nil } func (f *fakeDb) ModifyTodoDb(td todos.Todo) error { for _, t := range f.todos { - if t.Id == td.Id { + if t.ID == td.ID { t.Task = td.Task return nil } } + return nil } func (f *fakeDb) GetTodosDb(u users.User) (t []todos.Todo, e error) { @@ -104,10 +107,10 @@ func setupFakeDb() fakeDb { task4, _ := todos.NewTask("Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui") mdp, _ := users.HashPassword("123456") - todo1 := todos.Todo{Id: 1, Task: task1, Priority: 3} - todo2 := todos.Todo{Id: 2, Task: task2, Priority: 2} - todo3 := todos.Todo{Id: 3, Task: task3, Priority: 2} - todo4 := todos.Todo{Id: 12, Task: task4, Priority: 1} + todo1 := todos.Todo{ID: 1, Task: task1, Priority: 3} + todo2 := todos.Todo{ID: 2, Task: task2, Priority: 2} + todo3 := todos.Todo{ID: 3, Task: task3, Priority: 2} + todo4 := todos.Todo{ID: 12, Task: task4, Priority: 1} user, _ = users.NewUser("clem@caramail.fr", mdp) db.AddTodoDb(todo1, user) @@ -120,8 +123,13 @@ func setupFakeDb() fakeDb { func TestHandleSignin(t *testing.T) { db := setupFakeDb() - todos.Init(&db) - users.Init(&db) + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } var tests = []struct { name, entryEmail, entryPassword, confirmPassword, want string @@ -137,7 +145,7 @@ func TestHandleSignin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { urltxt := fmt.Sprintf("/signin?email=%v&password=%v&confirmpassword=%v", url.QueryEscape(tt.entryEmail), tt.entryPassword, tt.confirmPassword) - req := httptest.NewRequest(http.MethodPost, urltxt, nil) + req := httptest.NewRequest(http.MethodPost, urltxt, http.NoBody) w := httptest.NewRecorder() HandleSignin(w, req) res := w.Result() @@ -155,8 +163,13 @@ func TestHandleSignin(t *testing.T) { } func TestHandleLogin(t *testing.T) { db := setupFakeDb() - todos.Init(&db) - users.Init(&db) + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } var tests = []struct { name, entryEmail, entryPassword, want string @@ -171,7 +184,7 @@ func TestHandleLogin(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { urltxt := fmt.Sprintf("/login?email=%v&password=%v", url.QueryEscape(tt.entryEmail), tt.entryPassword) - req := httptest.NewRequest(http.MethodPost, urltxt, nil) + req := httptest.NewRequest(http.MethodPost, urltxt, http.NoBody) w := httptest.NewRecorder() HandleLogin(w, req) res := w.Result() @@ -190,25 +203,35 @@ func TestHandleLogin(t *testing.T) { func TestHandleLogout(t *testing.T) { db := setupFakeDb() - users.Init(&db) - req := httptest.NewRequest(http.MethodGet, "/logout", nil) + err := users.Init(&db) + + if err != nil { + t.Fatalf("Expected error to be nil but got : %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/logout", http.NoBody) req.AddCookie(&http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: "token", }) + w := httptest.NewRecorder() HandleLogout(w, req) res := w.Result() got := res.Cookies() + + defer res.Body.Close() + want := &http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: "", MaxAge: -1, Expires: time.Now().Add(-time.Hour), Path: "/", } found := false + for _, cookie := range got { if cookie.Name == want.Name { if cookie.Value == want.Value { @@ -217,6 +240,7 @@ func TestHandleLogout(t *testing.T) { } } } + if !found { t.Errorf("expected response to contain '%s', but got '%s'", want, got) } @@ -224,62 +248,74 @@ func TestHandleLogout(t *testing.T) { func TestHandleGetTodos(t *testing.T) { db := setupFakeDb() - todos.Init(&db) - users.Init(&db) + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } token := jsonwebToken(user) want := Todos2TodosView(db.todos) - req := httptest.NewRequest(http.MethodGet, "/todos", nil) + req := httptest.NewRequest(http.MethodGet, "/todos", http.NoBody) req.AddCookie(&http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: token, }) + w := httptest.NewRecorder() HandleGetTodos(w, req) res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) if err != nil { t.Errorf("expected error to be nil got %v", err) } - wantJson, err2 := json.Marshal(want) + + wantJSON, err2 := json.Marshal(want) if err2 != nil { panic(err2) } + got := string(body) - if !strings.Contains(got, string(wantJson)) { - t.Errorf("expected response to contain '%s', but got '%s'", string(wantJson), got) + if !strings.Contains(got, string(wantJSON)) { + t.Errorf("expected response to contain '%s', but got '%s'", string(wantJSON), got) } } func TestHandleAddTodo(t *testing.T) { db := setupFakeDb() - todos.Init(&db) - users.Init(&db) + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } token := jsonwebToken(user) var tests = []struct { - name string - entryTxt string - entryPrio int - want string + name, entryTxt, entryPrio, want string }{ - {"Cas normal", "Sortir le chien", 2, todos.GetTask(db.todos[1].Task)}, - {"Chaîne vide", "", 1, "veuillez renseigner du texte"}, - {"Caractères spéciaux autorisés", "(/$-_~+)=", 2, todos.GetTask(db.todos[2].Task)}, - {"Caractères spéciaux non autorisés", "(/$-_]&[~]%)=", 3, "caractères spéciaux non autorisés"}, - {"Plusieurs espaces en entrée", " ", 2, "veuillez renseigner du texte"}, - {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", 1, todos.GetTask(db.todos[3].Task)}, + {"Cas normal", "Sortir le chien", "2", todos.GetTask(db.todos[1].Task)}, + {"Chaîne vide", "", "1", "veuillez renseigner du texte"}, + {"Caractères spéciaux autorisés", "(/$-_~+)=", "2", todos.GetTask(db.todos[2].Task)}, + {"Caractères spéciaux non autorisés", "(/$-_]&[~]%)=", "3", "caractères spéciaux non autorisés"}, + {"Plusieurs espaces en entrée", " ", "2", "veuillez renseigner du texte"}, + {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", "1", todos.GetTask(db.todos[3].Task)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { urltxt := fmt.Sprintf("/add?task=%v&priority=%v", url.QueryEscape(tt.entryTxt), tt.entryPrio) - req := httptest.NewRequest(http.MethodPost, urltxt, nil) + req := httptest.NewRequest(http.MethodPost, urltxt, http.NoBody) req.AddCookie(&http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: token, }) w := httptest.NewRecorder() @@ -300,59 +336,87 @@ func TestHandleAddTodo(t *testing.T) { func TestHandleDeleteTodo(t *testing.T) { db := fakeDb{} - todos.Init(&db) + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + token := jsonwebToken(user) var tests = []struct { - name, entryTxt, entryId, want string + name, entryTxt, entryID, want string }{ {"Cas normal", "Blablabla", "3", "Blablabla"}, {"Chaîne vide", "", "123", "réessayez ultérieurement"}, - {"Id non numérique", "BlablaASupprimer", "azerty", "erreur de conversion"}, - {"Id vide", "BlablaASupprimer2", "", "erreur de conversion"}, + {"Id non numérique", "BlablaASupprimer", "azerty", ErrConv}, + {"Id vide", "BlablaASupprimer2", "", ErrConv}, {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", "10", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - url := fmt.Sprintf("/delete?id=%v&text=%v", tt.entryId, url.QueryEscape(tt.entryTxt)) - req := httptest.NewRequest(http.MethodPost, url, nil) + urltxt := fmt.Sprintf("/delete?id=%v&text=%v", tt.entryID, url.QueryEscape(tt.entryTxt)) + fmt.Print(urltxt) + req := httptest.NewRequest(http.MethodPost, urltxt, http.NoBody) + req.AddCookie(&http.Cookie{ + Name: CookieName, + Value: token, + }) w := httptest.NewRecorder() HandleDeleteTodo(w, req) res := w.Result() defer res.Body.Close() body, err := io.ReadAll(res.Body) + fmt.Print(body) if err != nil { t.Errorf("expected error to be nil got %v", err) } got := string(body) - if !strings.Contains(got, tt.want) && !strings.Contains(got, tt.entryId) { - t.Errorf("expected response to contain '%s' or '%s', but got '%s'", tt.want, tt.entryId, got) + fmt.Print(got) + if !strings.Contains(got, tt.want) && !strings.Contains(got, tt.entryID) { + t.Errorf("expected response to contain '%s' or '%s', but got '%s'", tt.want, tt.entryID, got) } }) } } func TestHandleModifyTodo(t *testing.T) { - db := fakeDb{} - todos.Init(&db) + db := setupFakeDb() + + err := todos.Init(&db) + err2 := users.Init(&db) + + if err != nil || err2 != nil { + t.Fatalf("Expected error to be nil but got : %v / %v", err, err2) + } + + token := jsonwebToken(user) var tests = []struct { - name, entryTxt, entryId, entryPrio, want string + name, entryTxt, entryID, entryPrio, want string }{ - {"Cas normal", "Blabliblou", "3", "2", "Blabliblou"}, + {"Cas normal", "Sortir le chien", "2", "2", todos.GetTask(db.todos[1].Task)}, + {"Caractères spéciaux autorisés", "(/$-_~+)=", "3", "2", todos.GetTask(db.todos[2].Task)}, {"Chaîne vide", "", "123", "1", "veuillez renseigner du texte"}, - {"Caractères spéciaux autorisés", "(/$-_~+)=", "13", "2", "(/$-_~+)="}, + {"Caractères spéciaux autorisés", "(/$-_~+)=", "13", "2", todos.GetTask(db.todos[2].Task)}, {"Caractères spéciaux non autorisés", "(/${}-_~+)=", "13", "3", "caractères spéciaux non autorisés"}, - {"Id non numérique", "BlablaAModifier", "azerty", "1", "erreur de conversion"}, - {"Id vide", "BlablaAModifier2", "", "2", "erreur de conversion"}, + {"Id non numérique", "BlablaAModifier", "azerty", "1", ErrConv}, + {"Id vide", "BlablaAModifier2", "", "2", ErrConv}, {"Plusieurs espaces en entrée", " ", "56", "2", "veuillez renseigner du texte"}, - {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", "2", "3", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui"}, + {"Chaîne longue", "Une chaine très longue mais sans caractères spéciaux, d'ailleurs ma mère me dit toujours que je suis spécial, ça va c'est assez long ? Bon aller on va dire que oui", "12", "1", todos.GetTask(db.todos[3].Task)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - url := fmt.Sprintf("/modify?id=%v&task=%v&priority=%v", tt.entryId, url.QueryEscape(tt.entryTxt), tt.entryPrio) - req := httptest.NewRequest(http.MethodPost, url, nil) + urltxt := fmt.Sprintf("/modify?id=%v&task=%v&priority=%v", tt.entryID, url.QueryEscape(tt.entryTxt), tt.entryPrio) + req := httptest.NewRequest(http.MethodPost, urltxt, http.NoBody) + req.AddCookie(&http.Cookie{ + Name: CookieName, + Value: token, + }) w := httptest.NewRecorder() HandleModifyTodo(w, req) res := w.Result() @@ -362,8 +426,8 @@ func TestHandleModifyTodo(t *testing.T) { t.Errorf("expected error to be nil got %v", err) } got := string(body) - if !strings.Contains(got, tt.want) && !strings.Contains(got, tt.entryId) { - t.Errorf("expected response to contain '%s' or '%s', but got '%s'", tt.want, tt.entryId, got) + if !strings.Contains(got, tt.want) { + t.Errorf("expected response to contain '%s', but got '%s'", tt.want, got) } }) } diff --git a/web/webusers.go b/web/webusers.go index 81b0e1e..f033978 100644 --- a/web/webusers.go +++ b/web/webusers.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log" "net/http" "strings" "time" @@ -18,13 +17,13 @@ type Claims struct { UserEmail string `json:"useremail"` } -const COOKIE_NAME = "cookie" -const SECRET_KEY = "codesecret123" +const CookieName = "cookie" +const SecretKey = "codesecret123" +const ErrNoCookie = "connexion expirée" +const ErrInvToken = "token invalide" +const MinSubTokenStr = 2 -const ERR_NOCOOKIE = "connexion expirée" -const ERR_INVTOKEN = "token invalide" - -var token_exp time.Time // Délai d'expiration du token : 1h +var tokenExp time.Time // Délai d'expiration du token : 1h var invalidatedTokens = make(map[string]bool) @@ -49,12 +48,13 @@ func HandleSignin(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + token := jsonwebToken(user) http.SetCookie(w, &http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: token, - Expires: token_exp, + Expires: tokenExp, SameSite: http.SameSiteStrictMode, }) } @@ -68,26 +68,29 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + token := jsonwebToken(user) http.SetCookie(w, &http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: token, - Expires: token_exp, + Expires: tokenExp, SameSite: http.SameSiteStrictMode, }) } func HandleLogout(w http.ResponseWriter, r *http.Request) { - tokenStr, err := getTokenString(r, COOKIE_NAME) + tokenStr, err := getTokenString(r, CookieName) if err != nil { - err = fmt.Errorf(ERR_INVTOKEN) + err = fmt.Errorf(ErrInvToken) http.Error(w, err.Error(), http.StatusForbidden) + return } + invalidateToken(tokenStr) http.SetCookie(w, &http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: "", MaxAge: -1, Expires: time.Now().Add(-time.Hour), @@ -107,53 +110,60 @@ func WrapAuth(handler http.Handler, info TokenInfo) http.HandlerFunc { if err == http.ErrNoCookie { http.SetCookie(w, &http.Cookie{ - Name: COOKIE_NAME, + Name: CookieName, Value: "", MaxAge: -1, Expires: time.Now().Add(-1 * time.Hour), Path: "/", }) - err = fmt.Errorf(ERR_NOCOOKIE) + + err = fmt.Errorf(ErrNoCookie) http.Error(w, err.Error(), http.StatusForbidden) } else { err = fmt.Errorf("err getToken") http.Error(w, err.Error(), http.StatusForbidden) } + return } + if !validateToken(tokenStr) { - err = fmt.Errorf(ERR_INVTOKEN) + err = fmt.Errorf(ErrInvToken) http.Error(w, err.Error(), http.StatusForbidden) + return } - if ok, err := isAuthenticated(tokenStr, info.PrivateKey); !(ok && err == nil) { - http.Error(w, err.Error(), http.StatusUnauthorized) + if ok, errAuth := isAuthenticated(tokenStr, info.PrivateKey); !ok || errAuth != nil { + http.Error(w, errAuth.Error(), http.StatusUnauthorized) return } - m := make(map[string]interface{}) + m := make(map[string]any) - err = parseTokenString(tokenStr, &m) + err = parseTokenString(tokenStr, m) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } + handler.ServeHTTP(w, r) } } -func parseTokenString(tokenStr string, v *map[string]interface{}) (err error) { +func parseTokenString(tokenStr string, v map[string]any) (err error) { encodedStrings := strings.Split(tokenStr, ".") - if len(encodedStrings) < 2 { + if len(encodedStrings) < MinSubTokenStr { err = http.ErrNoCookie - return + return err } + b, err := base64.RawURLEncoding.DecodeString(encodedStrings[1]) if err != nil { return err } - return json.Unmarshal(b, v) + + return json.Unmarshal(b, &v) } func getTokenString(r *http.Request, cookieName string) (string, error) { @@ -165,8 +175,8 @@ func getTokenString(r *http.Request, cookieName string) (string, error) { return cookie.Value, nil } -func isAuthenticated(tokenStr string, privateKey string) (bool, error) { - token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { +func isAuthenticated(tokenStr, privateKey string) (bool, error) { + token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } @@ -190,24 +200,27 @@ func validateToken(tokenStr string) bool { } func jsonwebToken(u users.User) string { - token_exp = time.Now().Add(time.Hour * 1) + tokenExp = time.Now().Add(time.Hour * 1) t := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ StandardClaims: jwt.StandardClaims{ - ExpiresAt: token_exp.Unix(), + ExpiresAt: tokenExp.Unix(), }, UserEmail: users.GetEmail(u), }) - token, err := t.SignedString([]byte(SECRET_KEY)) + + token, err := t.SignedString([]byte(SecretKey)) if err != nil { - log.Fatalln(err) + fmt.Print(err) } + return token } func getUserEmail(tokenStr string) string { claims := &Claims{} - jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { - return []byte(SECRET_KEY), nil + jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) { + return []byte(SecretKey), nil }) + return claims.UserEmail }