Disable a lot of code and start setting up tests for the authentication view

This commit is contained in:
Savanni D'Gerinel 2025-02-10 00:17:28 -05:00
parent 94a821d657
commit 4a0dc5b87a
12 changed files with 1613 additions and 2950 deletions

View File

@ -7,3 +7,8 @@ tasks:
- npm install - npm install
- npm run start - npm run start
test:
cmds:
- cd ../visions-types && task build
- npm install
- npm run test

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.17.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@testing-library/react": "^13.4.0", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^13.5.0", "@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.119", "@types/node": "^16.18.119",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
// render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -15,6 +15,7 @@ interface AppProps {
client: Client client: Client
} }
/*
const CandelaCharsheet = ({ client }: { client: Client }) => { const CandelaCharsheet = ({ client }: { client: Client }) => {
let [sheet, setSheet] = useState(undefined) let [sheet, setSheet] = useState(undefined)
useEffect( useEffect(
@ -24,21 +25,7 @@ const CandelaCharsheet = ({ client }: { client: Client }) => {
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div> return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
} }
*/
interface AuthedViewProps {
client: Client
}
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
const [state, manager] = useContext(StateContext)
return (
<Authentication onSetPassword={(password1, password2) => {
manager.setPassword(password1, password2)
}} onAuth={(username, password) => manager.auth(username, password)}>
{children}
</Authentication>
)
}
const App = ({ client }: AppProps) => { const App = ({ client }: AppProps) => {
console.log("rendering app") console.log("rendering app")
@ -64,10 +51,12 @@ const App = ({ client }: AppProps) => {
path: "/admin", path: "/admin",
element: <AdminView client={client} /> element: <AdminView client={client} />
}, },
/*
{ {
path: "/candela", path: "/candela",
element: <CandelaCharsheet client={client} /> element: <CandelaCharsheet client={client} />
}, },
*/
{ {
path: "/design", path: "/design",
element: <DesignPage /> element: <DesignPage />

View File

@ -4,7 +4,17 @@ export type PlayingField = {
backgroundImage: string; backgroundImage: string;
} }
export class Client { export type ServerResponse<A> = { type: "Unauthorized" } | { type: "Unexpected", status: number } | A;
export interface Client {
users: (sessionId: SessionId) => Promise<Array<UserOverview>>;
createUser: (sessionId: SessionId, username: string) => Promise<UserId>;
auth: (username: string, password: string) => Promise<ServerResponse<AuthResponse>>;
setPassword: (sessionId: SessionId, password_1: string, password_2: string) => Promise<void>;
logout: (sessionId: SessionId) => Promise<void>;
}
export class Connection implements Client {
private base: URL; private base: URL;
private sessionId: string | undefined; private sessionId: string | undefined;
@ -48,7 +58,7 @@ export class Client {
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) }); return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
} }
async users(sessionId: string) { async users(sessionId: string): Promise<Array<UserOverview>> {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = '/api/v1/users'; url.pathname = '/api/v1/users';
return fetch(url, { return fetch(url, {
@ -63,29 +73,31 @@ export class Client {
return fetch(url).then((response) => response.json()); return fetch(url).then((response) => response.json());
} }
async createUser(sessionId: string, username: string) { async createUser(sessionId: string, username: string): Promise<UserId> {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = '/api/v1/user'; url.pathname = '/api/v1/user';
return fetch(url, { const response: Response = await fetch(url, {
method: 'PUT', method: 'PUT',
headers: [['Authorization', `Bearer: ${sessionId}`], headers: [['Authorization', `Bearer ${sessionId}`],
['Content-Type', 'application/json']], ['Content-Type', 'application/json']],
body: JSON.stringify({ username }), body: JSON.stringify({ username }),
}).then((response) => response.json()) });
const userId: UserId = await response.json();
return userId;
} }
async setPassword(password_1: string, password_2: string) { async setPassword(sessionId: string, password_1: string, password_2: string) {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = `/api/v1/user/password`; url.pathname = `/api/v1/user/password`;
return fetch(url, { await fetch(url, {
method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ method: 'PUT', headers: [['Authorization', `Bearer ${sessionId}`], ['Content-Type', 'application/json']], body: JSON.stringify({
password_1, password_2, password_1, password_2,
}) })
}); });
} }
async auth(username: string, password: string): Promise<AuthResponse> { async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = `/api/v1/auth` url.pathname = `/api/v1/auth`
const response = await fetch(url, { const response = await fetch(url, {
@ -93,7 +105,22 @@ export class Client {
headers: [['Content-Type', 'application/json']], headers: [['Content-Type', 'application/json']],
body: JSON.stringify({ 'username': username, 'password': password }) body: JSON.stringify({ 'username': username, 'password': password })
}); });
return await response.json(); if (response.ok) {
return await response.json();
} else if (response.status == 401) {
return await response.json().then(() => ({ type: "Unauthorized" }));
} else {
return await response.json().then(() => ({ type: "Unexpected", status: response.status }));
}
}
async logout(sessionId: string) {
const url = new URL(this.base);
url.pathname = `/api/v1/auth`
await fetch(url, {
method: 'DELETE',
headers: [['Authorization', `Bearer ${sessionId}`]],
});
} }
async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserOverview | undefined> { async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserOverview | undefined> {

View File

@ -3,9 +3,9 @@ import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { Client } from './client'; import { Connection } from './client';
const client = new Client(); const client = new Connection();
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement

View File

@ -0,0 +1,52 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { UserOverview, AuthResponse, SessionId, UserId } from "visions-types";
import { Client, ServerResponse } from "../../client";
import { StateProvider } from "./StateProvider";
import { AuthedView, Authentication } from '../../views/Authentication/Authentication';
class MockClient implements Client {
validUsers: {[username: string]: string};
constructor() {
this.validUsers = { "vakarian": "aoeu", "shephard": "rubbish" }
}
async users(sessionId: SessionId): Promise<UserOverview[]> {
return [];
}
async createUser(sessionId: SessionId, username: string): Promise<UserId> {
return "abcdefg";
}
async setPassword(sessionId: SessionId, password_1: string, password_2: string): Promise<void> {
}
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
if (this.validUsers[username] === password) {
return { type: "Success", content: "session-id" };
} else {
return { type: "Unauthorized" };
}
}
async logout(sessionId: SessionId): Promise<void> {
}
}
test('a user is able to authenticate', async () => {
const client = new MockClient;
render(<StateProvider client={client}>
<AuthedView>
<p>Hi, authentication complete</p>
</AuthedView>
</StateProvider>);
expect(screen.getByText(/Welcome to Visions VTT/i)).toBeInTheDocument();
await act(async () => {
fireEvent.change(screen.getByPlaceholderText("Username"), { target: { value: "vakarian" } });
fireEvent.change(screen.getByPlaceholderText("Password"), { target: { value: "aoeu" } });
fireEvent.click(screen.getByRole("button"));
});
expect(screen.getByText(/Hi, authentication complete/i)).toBeInTheDocument();
})

View File

@ -62,26 +62,49 @@ export const getSessionId = (state: AppState): SessionId | undefined => {
} }
} }
class StateManager { interface StateManagerInterface {
client: Client | undefined; setPassword: (password1: string, password2: string) => void;
dispatch: React.Dispatch<Action> | undefined; auth: (username: string, password: string) => void;
logout: () => void;
createUser: (username: string) => void;
}
constructor(client: Client | undefined, dispatch: React.Dispatch<any> | undefined) { class NullManager implements StateManagerInterface {
constructor() { }
async setPassword(_password1: string, _password2: string) { }
async auth(_username: string, _password: string) { }
async logout() { }
async createUser(_username: string) { }
}
class StateManager implements StateManagerInterface{
client: Client;
state: AppState;
dispatch: React.Dispatch<Action>;
constructor(client: Client, state: AppState, dispatch: React.Dispatch<any>) {
this.client = client; this.client = client;
this.state = state;
this.dispatch = dispatch; this.dispatch = dispatch;
} }
async setPassword(password1: string, password2: string) { async setPassword(password1: string, password2: string) {
if (!this.client || !this.dispatch) return; let sessionId = getSessionId(this.state);
console.log(`StateManager.setPassword: ${sessionId}`);
await this.client.setPassword(password1, password2); if (sessionId) {
await this.client.setPassword(sessionId, password1, password2);
}
} }
async auth(username: string, password: string) { async auth(username: string, password: string) {
if (!this.client || !this.dispatch) return;
let authResponse = await this.client.auth(username, password); let authResponse = await this.client.auth(username, password);
switch (authResponse.type) { switch (authResponse.type) {
case "Unauthorized": break;
case "Unexpected": break;
case "Success": { case "Success": {
window.localStorage.setItem("sessionId", authResponse.content); window.localStorage.setItem("sessionId", authResponse.content);
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId: authResponse.content } }); this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId: authResponse.content } });
@ -99,24 +122,33 @@ class StateManager {
} }
} }
async createUser(username: string) { async logout() {
if (!this.client || !this.dispatch) return; const sessionId = getSessionId(this.state);
if (sessionId) {
await this.client.logout(sessionId);
window.localStorage.removeItem("sessionId");
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
}
}
const sessionId = window.localStorage.getItem("sessionId"); async createUser(username: string) {
console.log("order to createUser", username);
const sessionId = getSessionId(this.state);
if (sessionId) { if (sessionId) {
let createUserResponse = await this.client.createUser(sessionId, username); let createUserResponse = await this.client.createUser(sessionId, username);
console.log("createUser: ", createUserResponse);
} }
} }
} }
export const StateContext = createContext<[AppState, StateManager]>([initialState(), new StateManager(undefined, undefined)]); export const StateContext = createContext<[AppState, StateManagerInterface]>([initialState(), new NullManager()]);
interface StateProviderProps { client: Client; } interface StateProviderProps { client: Client; }
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => { export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
const [state, dispatch] = useReducer(stateReducer, initialState()); const [state, dispatch] = useReducer(stateReducer, initialState());
const stateManager = useRef(new StateManager(client, dispatch)); const stateManager = useRef(new StateManager(client, state, dispatch));
return <StateContext.Provider value={[state, stateManager.current]}> return <StateContext.Provider value={[state, stateManager.current]}>
{children} {children}

View File

@ -48,14 +48,14 @@ interface AdminProps {
export const AdminView = ({ client }: AdminProps) => { export const AdminView = ({ client }: AdminProps) => {
const [users, setUsers] = useState<Array<User>>([]); const [users, setUsers] = useState<Array<User>>([]);
/*
useEffect(() => { useEffect(() => {
client.users("aoeu").then((u) => { client.users("aoeu").then((u) => {
console.log(u); console.log(u);
setUsers(u); setUsers(u);
}); });
}, [client]); }, [client]);
*/
console.log(users);
return (<table> return (<table>
<tbody> <tbody>
{users.map((user) => <UserRow user={user} />)} {users.map((user) => <UserRow user={user} />)}

View File

@ -6,9 +6,10 @@ import './Authentication.css';
interface AuthenticationProps { interface AuthenticationProps {
onSetPassword: (password1: string, password2: string) => void; onSetPassword: (password1: string, password2: string) => void;
onAuth: (username: string, password: string) => void; onAuth: (username: string, password: string) => void;
onLogout: () => void;
} }
export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChildren<AuthenticationProps>) => { export const Authentication = ({ onSetPassword, onAuth, onLogout, children }: PropsWithChildren<AuthenticationProps>) => {
// No admin password set: prompt for the admin password // No admin password set: prompt for the admin password
// Password set, nobody logged in: prompt for login // Password set, nobody logged in: prompt for login
// User logged in: show the children // User logged in: show the children
@ -18,8 +19,6 @@ export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChi
let [pwField2, setPwField2] = useState<string>(""); let [pwField2, setPwField2] = useState<string>("");
let [state, _] = useContext(StateContext); let [state, _] = useContext(StateContext);
console.log("Authentication component", state.state);
switch (state.state) { switch (state.state) {
case LoadingState.Loading: { case LoadingState.Loading: {
return <div>Loading</div> return <div>Loading</div>
@ -39,7 +38,10 @@ export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChi
</div>; </div>;
} }
case "Authed": { case "Authed": {
return <div> {children} </div>; return (<div>
<div> <button onClick={onLogout}>Logout</button> </div>
<div> {children} </div>
</div>);
} }
case "PasswordReset": { case "PasswordReset": {
return <div className="auth"> return <div className="auth">
@ -48,7 +50,9 @@ export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChi
<p> Your password currently requires a reset. </p> <p> Your password currently requires a reset. </p>
<input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} /> <input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} />
<input type="password" placeholder="Retype your Password" onChange={(evt) => setPwField2(evt.target.value)} /> <input type="password" placeholder="Retype your Password" onChange={(evt) => setPwField2(evt.target.value)} />
<input type="submit" value="Submit" onClick={() => onSetPassword(pwField1, pwField2)} /> <input type="submit" value="Submit" onClick={() => {
onSetPassword(pwField1, pwField2);
}} />
</div> </div>
</div>; </div>;
} }
@ -59,5 +63,20 @@ export const Authentication = ({ onSetPassword, onAuth, children }: PropsWithChi
} }
} }
} }
} }
interface AuthedViewProps {}
export const AuthedView = ({ children }: PropsWithChildren<AuthedViewProps>) => {
const [_, manager] = useContext(StateContext)
return (
<Authentication onSetPassword={(password1, password2) => {
manager.setPassword(password1, password2)
}} onAuth={(username, password) => manager.auth(username, password)}
onLogout={() => manager.logout()}>
{children}
</Authentication>
)
}

View File

@ -15,6 +15,7 @@ export const MainView = ({ client }: MainProps) => {
const [users, setUsers] = useState<UserOverview[]>([]) const [users, setUsers] = useState<UserOverview[]>([])
const sessionId = getSessionId(state) const sessionId = getSessionId(state)
/*
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId) {
client.profile(sessionId, undefined).then((profile) => setProfile(profile)) client.profile(sessionId, undefined).then((profile) => setProfile(profile))
@ -24,6 +25,7 @@ export const MainView = ({ client }: MainProps) => {
}) })
} }
}, [sessionId, client]) }, [sessionId, client])
*/
return ( return (
<div> <div>