I'm using mock-socket to mock websockets calls.
I've got a test working but it only works for one test, then it says that the mock server is already listening on that url:
describe('mock socket method 1', () => {
let mockSocket;
let mockServer;
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win: Window): void {
// #ts-ignore
cy.stub(win, 'WebSocket', url => {
mockServer = new Server(url).on('connection', socket => {
console.log('mock socket connected');
mockSocket = socket;
});
if (!mockServer) return new WebSocket(url);
});
},
});
});
afterEach(() => {
mockSocket.close()
});
it('gets a message', () => {
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
it('gets a message', () => {
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
});
If I change the method to before() instead of beforeEach it works, but then I don't get a fresh environment for each test. I tried mockSocket.close() in afterEach() as you can see, but that doesn't work. I've tried cy.reload() but that gives a CORS error!
The error appears to be thrown at
> 15 | ReactDOM.render(
16 | <AppWrapper/>,
17 | document.getElementById('root'),
18 | );
AppWrapper is a AppContainer wrapped in redux's Provider, AppContainer connects App to redux, and here's App:
class App extends Component<AppProps> {
settingsSubscription: W3CWebSocket;
componentDidMount(): void {
// subscribe to websockets
this.settingsSubscription = this.subscribeToSettings(urls.SETTINGS_WS);
}
/**
* Sets up the websockets subscription to the settings.
* #param url the url of the websocket server
* #return the subscription object
*/
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
...
}
In the documentation they use mockServer.stop() to stop the mockServer instead of closing the mockSocket. That's what I am doing and most probably what you need as well.
Here's the snippet I am referring to:
// NOTE: this timeout is for creating another micro task that will happen after the above one
setTimeout(() => {
t.is(app.messages.length, 1);
t.is(app.messages[0], 'test message from mock server', 'we have subbed our websocket backend');
mockServer.stop(t.done);
}, 100);
Here's the link to the repo:
https://github.com/thoov/mock-socket/
Related
I'm playing around with Microservice architecture using NestJs. I've made a simplified repository with a few services that communicate over TCP with a mix of message and event patterns.
I have moved on to writing E2E tests for the using Supertest, and while I'm able to run the needed microservice, the requests respond with {"error": "There is no matching message handler defined in the remote service.", "statusCode": 500}
GatewayService: HTTP Rest Api where the E2E tests are run. Calls the service
AuthService: NestJs microservice running on 0.0.0.0:3001 by default
configService: a simple service that returns information needed to set up the services, like host and port. I have tried eliminating it from the test and hardcoding the values.
The E2E test file
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { ClientProxy, ClientsModule, Transport } from '#nestjs/microservices';
import { Test, TestingModule } from '#nestjs/testing';
import * as request from 'supertest';
import { configService } from '../src/config.service';
import { RpcExceptionFilter } from '../src/filters/rpc-exception.filter';
import { AppModule } from './../src/app.module';
describe('AuthenticationController (e2e)', () => {
let app: INestApplication;
let authClient: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
ClientsModule.register([
{
...configService.getServiceConfigs().authService,
transport: Transport.TCP,
},
]),
],
}).compile();
// Setup the app instance
app = moduleFixture.createNestApplication();
// Setup the relevant micorservice(s)
app.connectMicroservice({
transport: Transport.TCP,
name: configService.getServiceConfigs().authService.name,
options: configService.getServiceConfigs().authService.options,
});
app.startAllMicroservices();
// Add request validation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
// Add needed filters
app.useGlobalFilters(new RpcExceptionFilter());
await app.init();
authClient = app.get(configService.getServiceConfigs().authService.name);
await authClient.connect();
console.log('authClient', authClient);
});
describe('POST /auth/login', () => {
it('Should return status 200 and a user object with access token', () => {
return (
request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'exmple#user.com', password: 'password' })
// .expect(200)
.expect((response) => {
console.log('response', response.body);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('accessToken');
})
);
});
});
afterAll(async () => {
await app.close();
await authClient.close();
});
});
I have attempted adding a provider which I've used before when working with Grpc as the transport layer (this is TCP). Didn't change anything.
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
...
providers: [
{
provide: 'AUTH_SERVICE',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: 'localhost', port: 3001 },
});
},
},
],
I know that the microservice starts up and the gateway service is able to connect to it since when printing the authClient: Client proxy it returns a correct object with URL 0.0.0.0:3001. If I change the URL, or the name of the service in any part of the setup then errors about missing providers show, further confirming that it is supposedly correctly set up.
One of the best guides I've found on this matter. Sadly it doesn't work for my code.
I was learning some cypress from this video: https://www.youtube.com/watch?v=03kG2rdJYtc
I'm interested with he's saying at 29:33: "programatic login"
But he's using vue2 and Vuex.
My project is created with Vite and the state management is Pinia.
So how can I do a programatic login using the pinia action?
For example the welcome logged in user should see dashboard:
describe('Welcome', () => {
it('logged in user should visit dashboard', () => {
// login
cy.visit('/')
cy.url().should('contain', '/dashboard')
})
})
And my userStore:
export const useUserStore = defineStore({
id: 'user',
state: () => ({
username: ref(useLocalStorage('username', null)),
}),
getters: {
isLoggedIn: (state) => state.username !== null,
},
actions: {
login(username, password) {
return useAuthLoginService(username, password)
.then((response) => {
this.username = response.username
})
.catch((error) => {
return Promise.reject(new Error(error))
})
},
},
})
How can I call the login action on the cypress test?
For now as a workaround I'm writing on a localstorage like:
localStorage.setItem('username', 'user')
And it works fine, because userStore catch this item from localstorage and passes like it's logged in... But I don't like this solution, seems fragile, and I'd like to use the action which is made for login users.
Another thing I tried is adding the app variable inside window but it doesn't work for me... don't understand why...
on main.js
The video shows that code:
const vue = new Vue({...})
if(window.Cypress){
window.app = app
}
In my case it's:
const app = createApp(App)
if(window.Cypress){
window.app = app
}
But in cypress tests the window.app it's undefined... I don't know how I would access to userStore using this... like it was vuex.
Using the Pinia demo app as an example:
The store is initialized in App.vue. Add a reference to the newly created store(s) for Cypress to use
export default defineComponent({
components: { Layout, PiniaLogo },
setup() {
const user = useUserStore()
const cart = useCartStore()
if (window.Cypress) {
window.store = {user, cart) // test can see window.store
}
...
In the test
let store;
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`)
.then(win => store = win.store) // get app's store object
})
it('works', () => {
cy.wait(500) // wait for the JS to load
.then(() => store.cart.addItem('Cypress test item')) // invoke action
.then(() => {
const item1 = store.cart.items[0] // invoke getter
cy.wrap(item1)
.should('have.property', 'name', 'Cypress test item') // passes
})
The login action is asynchronous, so return the promise to allow Cypress to wait.
// user.js
async login(user, password) {
const userData = await apiLogin(user, password)
this.$patch({
name: user,
...userData,
})
return userData // this returns a promise which can awaited
},
// main.spec.js
describe('Pinia demo with counters', () => {
beforeEach(() => {
cy.viewport(1000, 1000)
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store before login
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging in
store.user.login('ed', 'ed').then(() => { // wait for API call
cy.wrap(store.user.name).should('eq', 'ed')
})
})
})
Alternatively, wait for the name to change on the page
// main.spec.js
cy.visit(`http://localhost:${PORT}`).then(win => {
store = win.store
// default name in store
cy.wrap(store.user.name).should('eq', 'Eduardo')
// logging on
store.user.login('ed', 'ed')
cy.contains('Hello ed') // waits for name on page to change
.then(() => {
cy.wrap(store.user.name).should('eq', 'ed')
})
})
My current setup is as such:
I have connected to my postgres database and i also have the websocket connected fine to the frontend.
But I am not sure how to use the postgres-adapter as its suggested in the documentation:
They say that you make a pool instead of a Client - thats fine for the db - then you say you use the io.adapter(createAdapter(pool));
The main functionality I want to get at is - when a change occurs on the postgresdb - then a notification gets send out just as I have currently - but then it should be connected inside the websocket for the websocket to send a message with the data to all connected clients. Has anyone implemented this?
client.connect(err => {
if (err) {
console.log("Error in connecting database: ", err);
} else {
console.log("Database connected");
client.on('notification', (msg) => {
console.log(msg.payload);
console.log(msg)
});
const query = client.query("LISTEN update_notification");
}
})
const webSocketSetup = (server) => {
var io = new Server(server, {
cors: {
origin: [
'http://localhost:3000',
]
}
});
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('disconnect', () => {
console.log('user disconnected');
});
socket.on('my message', (msg) => {
"" I Want the postgres Notification to be setup here. ""
});
});
}
I'm trying to use useEffect in my React app but also refactor things more modularly. Shown below is the heart of actual working code. It resides in a Context Provider file and does the following:
1. Calls AWS Amplify to get the latest Auth Access Token.
2. Uses this token, in the form of an Authorization header, when an Axios GET call is made to an API Endpoint.
This works fine but I thought it would make more sense to move Step #1 into its own useEffect construct above. Furthermore, in doing so, I could then also store the header object as its own Context property, which the GET call could then reference.
Unfortunately, I can now see from console log statements that when the GET call starts, the Auth Access Token has not yet been retrieved. So the refactoring attempt fails.
useEffect(() => {
const fetchData = async () => {
const config = {
headers: { "Authorization":
await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert('Error getting authorization token: '.concat(error))
})
}};
await axios.get('http://127.0.0.1:5000/some_path', config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert('Error getting data from endpoint: '.concat(error));
});
};
fetchData();
}, [myContextObject.some_data]);
Is there a way of refactoring my code into two useEffect instances such that the first one will complete before the second one starts?
You could hold the config object in a state. This way you can separate both fetch calls and trigger the second one once the first one finished:
const MyComponent = props => {
const myContextObject = useContext(myContext);
const [config, setConfig] = useState(null);
useEffect(() => {
const fetchData = async () => {
const config = {
headers: {
Authorization: await Auth.currentSession()
.then(data => {
return data.getAccessToken().getJwtToken();
})
.catch(error => {
alert("Error getting authorization token: ".concat(error));
})
}
};
setConfig(config);
};
fetchData();
}, [myContextObject.some_data]);
useEffect(() => {
if (!config) {
return;
}
const fetchData = async () => {
await axios
.get("http://127.0.0.1:5000/some_path", config)
.then(response => {
// Process the retrieved data and populate in a Context property
})
.catch(error => {
alert("Error getting data from endpoint: ".concat(error));
});
};
fetchData();
// This should work for the first call (not tested) as it goes from null to object.
// If you need subsequent changes then youll have to track some property
// of the object or similar
}, [config]);
return null;
};
With the normal graphql server we can define the context object like this:
app.use('/graphql', graphqlExpress(async (req) => {
return {
schema,
context: {
app,
params,
}
};
}));
** subscription server **
How can I do the same for the subscription server? (Doing the hybrid http / websocket approach). Can't seem to find a solution from the docs.
new SubscriptionServer({
execute,
subscribe,
schema,
onConnect: (connectionParams, webSocket) => {
console.log(connectionParams);
}
}, {
server,
path: '/subscriptions'
});
You can add a middleware before the execute function and add the required context before resolving the subscription.
It could look like this:
const middleware = (args) => new Promise((resolve, reject) => {
const [schema, document, root, context, variables, operation] = args;
context.app = <your app parameter>;
context.params = <your params>;
resolve(args);
});
SubscriptionServer.create({
schema,
subscribe,
execute: (...args) => middleware(args).then(args => { return execute(...args); }) },
{
server: httpServer,
path: "/subscription",
},
);
As you can see you have all the data from the request in the args of the execute function.