package main import ( "encoding/json" "net" "net/http" "testing" "time" ) // arrancaHub levanta un hub con un token dado y devuelve su direccion. func arrancaHub(t *testing.T, token string) (*peerRegistry, string) { t.Helper() reg := newPeerRegistry() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } mux := http.NewServeMux() mux.HandleFunc("/ws", reg.wsHandler(token)) srv := &http.Server{Handler: mux} go srv.Serve(ln) t.Cleanup(func() { srv.Close() }) return reg, ln.Addr().String() } func peers(reg *peerRegistry) int { return len(reg.views()) } // El token correcto entra y sus datos llegan. func TestTokenCorrectoEntra(t *testing.T) { reg, addr := arrancaHub(t, "el-bueno") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() if err := authenticate(c, config{Token: "el-bueno", clientID: "abc"}); err != nil { t.Fatalf("deberia haber autenticado: %v", err) } msg, _ := json.Marshal(peerMessage{Type: "state", ID: "abc", Name: "Fulano", Deaths: 7}) if err := c.WriteText(msg); err != nil { t.Fatal(err) } esperarHasta(t, func() bool { return peers(reg) == 1 }, "el peer nunca aparecio") if got := reg.views()[0].Deaths; got != 7 { t.Fatalf("esperaba 7 muertes, dio %d", got) } } // El token equivocado NO entra y no deja rastro. func TestTokenEquivocadoNoEntra(t *testing.T) { reg, addr := arrancaHub(t, "el-bueno") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() _ = authenticate(c, config{Token: "el-malo", clientID: "x"}) msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999}) c.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { t.Fatalf("el hub acepto datos con el token equivocado: %+v", reg.views()) } } // Mandar datos SIN autenticarse tampoco funciona. func TestSinAutenticarNoEntra(t *testing.T) { reg, addr := arrancaHub(t, "el-bueno") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999}) c.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { t.Fatalf("el hub acepto datos sin autenticacion: %+v", reg.views()) } } // El token no viaja: lo unico que sale del peer es un HMAC del desafio. func TestElTokenNoViajaPorLaRed(t *testing.T) { _, addr := arrancaHub(t, "secreto-muy-secreto") c, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c.Close() raw, err := c.ReadMessage() // el desafio if err != nil { t.Fatal(err) } var ch authChallenge json.Unmarshal(raw, &ch) if ch.Nonce == "" { t.Fatal("el hub no mando desafio") } reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofFor("secreto-muy-secreto", ch.Nonce)}) if string(reply) == "" { t.Fatal("respuesta vacia") } for _, b := range [][]byte{raw, reply} { if contiene(string(b), "secreto-muy-secreto") { t.Fatalf("el token aparece en el trafico: %s", b) } } } // Una respuesta capturada no sirve en otra conexion: el desafio cambia. func TestNoSePuedeRepetirUnaRespuestaVieja(t *testing.T) { reg, addr := arrancaHub(t, "el-bueno") c1, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } raw, _ := c1.ReadMessage() var ch1 authChallenge json.Unmarshal(raw, &ch1) proofViejo := proofFor("el-bueno", ch1.Nonce) c1.Close() c2, err := wsDial(addr, "/ws", 3*time.Second) if err != nil { t.Fatal(err) } defer c2.Close() raw2, _ := c2.ReadMessage() var ch2 authChallenge json.Unmarshal(raw2, &ch2) if ch2.Nonce == ch1.Nonce { t.Fatal("el desafio se repitio entre conexiones: el nonce tiene que ser distinto") } reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofViejo}) c2.WriteText(reply) msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Repetidor", Deaths: 1}) c2.WriteText(msg) time.Sleep(700 * time.Millisecond) if peers(reg) != 0 { t.Fatalf("el hub acepto una respuesta repetida: %+v", reg.views()) } } // El token generado es aleatorio y de largo razonable. func TestTokenGeneradoEsFuerte(t *testing.T) { testExeDir = t.TempDir() a, gen, err := resolveToken(config{}) if err != nil || !gen { t.Fatalf("deberia haber generado uno: gen=%v err=%v", gen, err) } if len(a) != tokenBytes*2 { t.Fatalf("largo inesperado: %d", len(a)) } // la segunda vez lo lee del archivo, no genera otro b, gen2, _ := resolveToken(config{}) if gen2 || a != b { t.Fatalf("deberia reusar el guardado: %q vs %q (gen=%v)", a, b, gen2) } // y en otra instalacion sale distinto testExeDir = t.TempDir() c, _, _ := resolveToken(config{}) if c == a { t.Fatal("dos instalaciones generaron el mismo token") } testExeDir = "" } func contiene(s, sub string) bool { return len(sub) > 0 && len(s) >= len(sub) && (func() bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false })() } func esperarHasta(t *testing.T, cond func() bool, msg string) { t.Helper() deadline := time.Now().Add(4 * time.Second) for time.Now().Before(deadline) { if cond() { return } time.Sleep(50 * time.Millisecond) } t.Fatal(msg) }